Redux is something you really need to know if you are going to do anything professionally with JS and especially React. For some time it seemed quite complex with a lot of boilerplate so I mostly used MobX and more recently React context.
However, my curiosity got better of me and I had to dig a bit deeper to comprehend the great Redux. In this post I will try to simplify basic concepts of how Redux works so you can try and not just build but also comprehend a React-Redux app.
What is Redux?
"Redux is a predictable state container for JavaScript apps." (https://redux.js.org/introduction/getting-started). It is a place that manages the state and makes changes according to the provided actions.
What is it for?
For use cases when you need to have data available across the application i.e. when passing data through props is not possible.
Why is it powerful?
Redux is highly predictable which makes debugging much easier since you know what is happening where. It is also scalable so it is a good fit for production grade apps.
Brief overview
Let's say you're making an app that increments the count. This app has:
- Count value,
- Increment button,
- Decrement button,
- Change with value,
What is then happening?
When you want to increment a count, you dispatch an action. This action then through special function called reducer takes the previous state, increments it and returns it. Component that listens through Selector
re-renders on change of state.
Let's go to the code
In order to create the "Counter" app with React and Redux, we need to add following packages to your React app (I will assume you know how to create a basic Create React App):
yarn add @reduxjs/toolkit react-redux
Now the first thing we will do is to create a Store and provide it to the entry point of your App, in this case it is Index.js
/src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
export const Store = configureStore({
});
Here we are using configureStore
from Redux toolkit which is a function that requires passing a reducer. We will get back to it in a second.
/index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import { Store } from "./app/store";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Provider store={Store}>
<App />
</Provider>
</StrictMode>,
rootElement
);
Here we are using Provider
to provide our Redux store to all wrapped Components.
Believe it or not, we are half way there!
Next, we need to populate the core of our Redux logic and that is the Slice. You can think of Slice as a collection of Redux reducer logic & actions for a single feature in the app.
(in a blogging app there would be separate Slices for users, posts, comments etc.).
Our Slice will contain:
- Initial value
- Increment logic
- Decrement logic
- Change by value logic
Let's go:
/src/features/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const Slice = createSlice({
name: "counter",
initialState: {
},
reducers: {
}
});
First we have a named import for createSlice
from toolkit. In this function we are giving it a name, setting initial state, and providing logic as reducers.
/src/features/counterSlice.js
...
export const Slice = createSlice({
name: "counter",
initialState: {
value: 0
},
...
Here we set the initial state to 0, every time we refresh our application it will be defaulted to 0. More likely scenario here would be fetching the data from external source via async function. We won't be covering that here but you can read more about async logic with Thunks
.
In our reducers object we will have increment, decrement, and changeByValue:
/src/features/counterSlice.js
...
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
changeByValue: (state, action) => {
state.value += action.payload;
}
}
...
Now it starts to make sense. When we dispatch an action from our component we are referencing one of these in the reducers object. Reducer is acting as an "event listener" that handles events based on received action type while Dispatching actions is "triggering events".
With increment
and decrement
we are updating state value, while changeByValue
takes action payload to determine the exact value of that update.
Only thing left to do in the slice is to export Actions, State reducer, and state value. Here is a complete file
/src/features/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const Slice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
changeByValue: (state, action) => {
state.value += action.payload;
}
}
});
export const selectCount = (state) => state.counter.value;
export const { increment, decrement, changeByValue } = Slice.actions;
export default Slice.reducer;
Important note here is that Reducers are not allowed to modify existing state. They have to make immutable updates which basically means copying the state and modifying that copy. Here createSlice()
does the heavy-lifting for us and creates immutable updates, so as long you are inside createSlice()
you are good with immutability rule 👌
We now need to update our store with reducers we made:
/src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counterSlice";
export const Store = configureStore({
reducer: {
counter: counterReducer
}
});
The only thing left to do is to create a component that will be the UI for our app:
/src/features/Counter.js
import React, { useState } from "react";
const Counter = () => {
return (
<>
<h1>Counter app</h1>
<p>Count: </p>
<button>Increment</button>
<button>Decrement</button>
<button>
Change by Value
</button>
<input/>
</>
);
};
export default Counter;
We are starting from this base. We will need a way to:
- Show current count status
- Increment on click of button
- Decrement on click of button
- Input value for change
- Apply value to the count
We have already exported the current state from the Slice like this:
/src/features/counterSlice.js
export const selectCount = (state) => state.counter.value;
We can now use this to show current value using useSelector()
/src/features/Counter.js
...
import { useSelector } from "react-redux";
import { selectCount } from "./counterSlice";
const Counter = () => {
const count = useSelector(selectCount);
return (
<>
...
<p>Count: {count}</p>
...
</>
);
...
As we mentioned earlier, we will use useDispatch()
to dispatch actions we need -> increment, decrement, changeByValue:
/src/features/Counter.js
...
import { useDispatch, useSelector } from "react-redux";
import {
increment,
decrement,
changeByValue,
selectCount
} from "./counterSlice";
const Counter = () => {
const count = useSelector(selectCount);
const dispatch = useDispatch();
return (
<>
...
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(changeByValue(value))}>
Change by Value
</button>
...
</>
);
};
...
Increment and Decrement are pretty much self-explanatory, but with changeByValue we have a variable value
that we need to define in order to send it as a payload. We will use React local state for this with onChange
and handleChange()
to set this value properly. With those additions we have a complete component:
/src/features/Counter.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
increment,
decrement,
changeByValue,
selectCount
} from "./counterSlice";
const Counter = () => {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [value, setValue] = useState();
const handleChange = (e) => {
const num = parseInt(e.target.value);
setValue(num);
};
return (
<>
<h1>Counter app</h1>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
<button onClick={() => dispatch(changeByValue(value))}>
Change by Value
</button>
<input onChange={(e) => handleChange(e)} />
</>
);
};
export default Counter;
With this addition, we have a working React Redux app. Congrats! You can install Redux dev tools to your browser to see what is exactly happening and how actions mutate the state.
Recap
After seeing how everything connects together, here is the recap of the update cycle that happens when the user clicks a button to increment/decrement count:
- User clicks a button
- App dispatches an action to Redux store
- Store runs reducer function with previous state and current action after which it saves return value as the new state
- Store notifies all subscribed parts of the UI
- Each UI component that needs data checks if it is what it needs
- Each UI component that has its data changed forces re-render with the new data
Diving into Redux might seem daunting but once you get hang of basic principles it becomes a powerful weapon in your coding arsenal.
Thank you for reading,
'Take every chance to learn something new'
Top comments (10)
I think redux-toolkit is just plain ugly. The only nice part is that integrates with react hooks. It is just too little for platform "independent" library.
it states solving 3 problems:
And I don't thing they were problems at first place and even if they were (for some) it does not solve it.
You've complained in multiple threads that RTK is "ugly". What, specifically, do you think is "ugly" about it?
Also, RTK does solve all three of those issues:
applyMiddleware
, check forwindow.__DEVTOOLS_EXTENSION__
,compose()
them together, etc.redux-thunk
,immer
, andreselect
, so you only have to add@reduxjs/toolkit
yourself.createAsyncThunk
andcreateEntityAdapter
for simplifying standard use cases.If you don't like Immer's "magic", that's a valid opinion and up to you. But as the primary Redux maintainer, I've seen all the problems and pain points people have had with Redux over the years. RTK does solve those problems, and the huge amount of positive feedback we've gotten about RTK reflects that.
It's my right to complain :) BTW thumbs up for the good work. I think redux was bulls eye and it changed FE application development forever. However I have my opinions about opinionated toolset.
redux
I haveredux-thunk
,immer
,reselect
andredux-toolkit
in my dependency list. I findimmer
on verge of hating. to me it is not readable enough. I usually use plain js orramda
.Immer
just hides things assuming that is what people want. Helping by hiding is not great approach.If I compare my reducers and selectors against something written using
redux-toolkit
I fail to see any excessive what you call boilerplate code.Examples of things I do not like:
I do not like action creators coupled with reducer. It is just a function and You could hardly find anything simpler than that.
redux-toolkit
takes that simplicity away.Why do you see the need for a factory method for a function?
I do not like the
createAsyndThunk
.redux-thunk
is by definition async. I believe that catering for developers who do notunderstand 7 lines of code (redux-thunk) by creating convoluted API for redux-thunk is just too much.
Agreed that store setup is typically a once-per-project kind of thing. But, it's something you have to do for every project, and being able to do that in one function call, and get correct defaults out of the box (including dev mode checks that catch common errors) is a big improvement.
I'm really not understanding what you don't like about Immer. For comparison:
Writing reducers with Immer results in much less code, and much clearer intent as to how you're actually trying to update state. Also, hand-written immutable updates are very prone to errors, and accidental mutations have always been the #1 cause of bugs with Redux. Immer eliminates those completely.
So, between making the code shorter and easier to read, and eliminating accidental mutations, Immer is a huge improvement for Redux users.
We have always encouraged the user of action creators as a standard practice with Redux, and I wrote a separate post on "why use action creators?" several years ago. They provide a consistent interface for dispatching actions, and also encapsulate any logic needed to set up the action object (such as generating unique IDs or formatting parameters). With RTK, you get them for free anyway, so there's no reason not to use them.
This is incorrect. A thunk may contain any logic, sync or async, and you can still write them by hand. However, we have always encouraged the "dispatch pending/fulfilled/rejected actions" pattern, and that requires additional work: defining the action types and action creators for all three cases, and writing the logic to dispatch those actions at the right times with the right contents. So, it's a lot more than "7 lines" for each thunk if you're writing all those action creators and action types by hand.
createAsyncThunk
does all that for you.1.) purposfully written example. It is rather unusual to have action targeting four levels. You could run into few worse problems than verbose code. like change in API contract if that is what defines the shape of your state object.
But if I had to I would use my favourit little functional library and it would look e.g. like this
as opposed to:
the beauty of the first version is that it returns the new state object. your example does not return anything and relies on immer's internal magic to figure it out
That impacts your tests and the general readability of your code.
2.) redux-thunk - what I mean by definition - perhaps wrong wording - you define the function which is executed in the middleware and returned. whether the function is async should be of no concern to redux or toolkit
so if you define function which returns promise then you know what you are doing. I can't see how is this difficult. Why does this justify a wrapper which is 50 times bigger than
redux-thunk
itself?I meant
redux-thunk
logic is in 7 lines. however dispatching right actions with right content at the right time - you still need to take care of that whether you useredux-thunk
,toolkit
or your own middleware.I only can speak for myself when I say I've never seen the above mentioned three problems which toolkit is trying to address as problems at the first place. It is maybe be because I've been already addressing them unconsciously similar way as I address similar problems outside react-redux world. But for me and like minded developers it might feel like toolkit is being forced down the thought by statements like
FWIW, I can tell you I have seen all of these problems pop up, thousands of times over the years. I wouldn't have created RTK if these problems didn't exist. (and that includes seeing some very deeply nested immutable update logic - that example is fictional, but I've seen multiple cases at least that long in real production code.)
And yes, we are telling people that RTK is the right way to use Redux at this point, because it solves all of these problems, and the highly positive feedback we've gotten reflects that.
Just a couple days ago I got this DM:
Some other similar responses:
1)
2)
3)
I'm not saying that you personally must like RTK or use it. I am saying that it does solve the problems that most Redux users have experienced, and that almost everyone who has used RTK loves it.
Well, I'm not arguing that many find it easier with
redux-toolkit
. You(as toulkit devs) obviously have vision and aim which is great. And you have a feedback from thousands of users and if this is how the community wants to drive it so be it. :)I only expressed my own experience and my own opinion. Thanks for the discussion anyway, All the best!
I think a little bit of FP functions can help with all the pain with Redux.
Here is my attempt to simplify (I tried not to use even Ramda):
codesandbox.io/p/devbox/github/sul...
I don’t encourage using Redux in new projects as there are many better solutions. It worth when it comes to legacy code that we want to improve.
Agreed, it does tend to use more "magic" approach, I can't see anything wrong or more complex if one wants to use Redux without toolkit package. Thanks for the insight.
Agreed