For some time, Redux is a thing that has bugged me because I know it, but I felt it was not sufficient. Because of this, I decided to study more and explain here what my studies led me to.
In the beginning, I think one of my biggest problems with Redux was not the difficulty, but I was trying to learn a lot of things at the same time, Redux, React, CSS, and why not, improve in JS. Because of this, I was overwhelmed by the amount of content and ended up with gaps in my knowledge.
State management and a single source of truth
According to the React documentation, the state is similar to props but is private and fully controlled by the component. In frontend applications, it’s expected that components share some state, and each library has its way of handling this. One thing that can happen in all of these libraries is with the growth of the application, state management becomes more complicated because of the quantity and complexity of the states, and this can lead us to the prop drilling¹.
Prop drilling is the process you need to do to get the data through your application. The problem starts when some components need to handle a lot of states to pass them down to other components without really using the data. Redux can solve this problem in different forms, but in this article, I will give a single source of truth as one of them.
A single source of truth is one place to store all your state that needs to be shared between multiple components, and this place can be trusted to have the correct state. With this, we can avoid most parts or all of the problems in prop drilling.
Why Redux?
We can use Redux to simplify state management since it implements the concept of a single source of truth. Also, an essential factor is that Redux does not depend on any specific framework so that it can be used in any frontend application.
What is Redux?
According to official documentation, Redux is a predictable, centralized, debuggable, and flexible JS library. Let’s go over these definitions and see what they mean:
- Predictable: Redux follows a couple of functional programming practices, like immutability and deterministic functions, making it easier to predict the outcome at each interaction with it.
Centralized: with Redux, you can centralize all your application states in just one place, enforcing the concept of a single source of truth.
Debuggable: you can easily add debugging tools to Redux, like Redux DevTools in the browser. It relies on the immutability to let you do “time-travel debugging.”
Flexible: because it is an independent JS library, you can use redux in everything where Javascript can be written, every popular frontend framework, even with node.
How does Redux work?
To be brief, Redux creates an object we’ll call the store, which will hold your data, and we will request every change to this data from the store. Every request to change this data will be defined as actions, which are objects containing a string with the type of the change and the payload required to perform it. At the moment of the creation of the store, we’ll pass a reducer function as an argument, which will be used internally by the store to handle each action one at a time. Every action sent to the store (we call this “dispatching and action”) will be passed alongside the current state of your data to the reducer function, which will be responsible for applying that change to the current state and returning the data after the change.
Now to better understand, I will show one example of a minimal Redux setup. To set up Redux in our project, we first need to create a store. The Redux team recommends using the configureStore
from redux-toolkit, but to make the explanation more manageable, I will use the legacy_createStore
because it has fewer abstractions.
To set up a new store, you will need to import from redux and pass a reducer function:
import { legacy_createStore } from "redux";
const initialState = { value: 0 };
const reducer = (state = initialState, action) => {
if (action.type === "ADD") {
return { counter: state.value + 1 };
}
return state;
};
const store = legacy_createStore(reducer);
In this case, our reducer function can receive the type “ADD” action and return the state value +1. In any other case, this action won’t be recognized and doesn’t affect our state, so we will return our form as the default case.
If we want to change the data in our store, we will need action. In this case, with the type “ADD,” it is a convention to use all uppercase, but you don’t need to do it.
const addAction = { type: 'ADD' }
To send this action to your store, we can call the store’s dispatch method, which will handle it internally to change the state.
store.dispatch(addAction)
Now our value from the store must have changed to 1, and we can confirm this with another store method called getState.
console.log(store.getState())
// { counter: 1 }
So this is a minimal example using just Redux with pure JS. When using it alongside React and Redux Toolkit, we get some other abstractions that are more ergonomic for that usage.
Redux and React
Now let’s see how to use Redux inside a React application, exemplified by a scenario where we manage the value of a counter. You can find it in this GitHub link.
The first thing we will need to do is set up our reducer, so create a reducer.js file in our src/ folder and type:
// reducer.js
const initialState = { count: 0 };
export const reducer = (state = initialState, action) => {
if (action.type === 'INCREMENT') {
return { count: state.count + 1 };
}
if (action.type === 'DECREMENT') {
return { count: state.count - 1 };
}
return state;
};
Here we are setting our initial state and creating our reducer function, which can handle two types of actions. As explained before, if we can’t take some action, we return the original value of the state.
Now let’s set up our store. For this, you will need the file store.js in the src folder.
// ./store.js
import { reducer } from './reducer';
import { legacy_createStore } from 'redux';
export const store = legacy_createStore(reducer);
And now we make the rest of the application capable of interacting and depending on the Redux store by using a provider. Change the index.js to be like this:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './store';
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
The next step will be our Counter component to read the state managed by the store and dispatch actions after some user interaction. To read the state, we’re going to use the useSelector hook, and to dispatch actions, we have the useDispatch one. Both of them are provided by the react-redux library. These hooks are abstractions around the dispatch and getState store methods we mentioned.
So first let’s import useDispatch and useSelector:
import { useDispatch, useSelector } from 'react-redux';
Next, let’s change our const count to use the useSelector.
const count = useSelector((state) => state.count);
In this part, our count already must be connected with Redux, so if you change your initial state and update the page, the number must change too. Afterward, let’s use the dispatcher to send actions to our store and increment/decrement our counter.
Now we will need to create a constant called dispatch containing the return of the useDispatch hook and use it with the correct actions in our buttons. After the change, our Counter component must look like this:
import { useDispatch, useSelector } from 'react-redux';
export const Counter = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
</>
);
};
export default Counter;
And this is an example of how to use Reducer in React. Remember that we used the legacy implementation of createStore for learning purposes, but now the official recommendation is to use Redux Toolkit.
In which cases should I use Redux?
According to official docs, the most useful cases are when you have big or medium size applications which need to update the state frequently, when you have complex state logic to update or when you need to see how the state is being updated over time.
Redux is excellent in these cases, but it can increase your application development time initially because everyone will need to learn and stay alert with Redux, so it is not recommended for a small and simple application. Which could be using a more straightforward approach like React context + useReducer.
Where to go from here?
From here, I recommend taking a look at the Redux course from Dan Abramov, one of its creators, to know more about React context and Redux differences you can check here, and I recommend checking the official documentation too, they have a nice tutorial showing how it works.
Refs:
We want to work with you. Check out our "What We Do" section!