Get to know Redux in 2021
Semir Teskeredzic
Posted on May 3, 2021
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'
Posted on May 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.