Using Hooks to Replace Redux
Make your life easier with lifecycle hooks and state management in functional components
As a beginner React developer coming from Vue, I found myself struggling with Redux and all the boilerplate needed to make simple state management the right way: action types, action builders, the proper actions, and reducers. If you want to connect it to React the easy way you have to install another package — react-redux — and learn how to use it. After that, if you want to use async actions, you have to write your own redux middleware or learn how to use another package — redux-thunk…
But then React comes along with hooks! Hooks are magical little things that let you use class-based components features, like lifecycle hooks and state management in functional components, with much greater ease.
One of these hooks is
useReducer. As the official documentation states,
Accepts a reducer of type
(state, action) => newState, and returns the current state paired with a
dispatchmethod. (If you’re familiar with Redux, you already know how this works.)
I wonder — maybe Redux is mentioned for a reason, and I could use this to make centralized state management just like Redux. And why not use
useState? It does almost the same thing but in a different way. React documentation states that when called
useState returns a stateful value and a function to update it.
There’s one fundamental difference that I think makes
useReducer and not
useState most suitable when talking about centralized state management. With the
useState hook, the business logic responsible for computing the new state would have to be inside all the components that would update the state. With
useReducer we can have all the business logic in one place (the reducer itself), making it easy to maintain and keeping the components focused on what they were primarily designed to do. Another bonus is that the
useReducer hook keeps the “dispatching an action to update the state” pattern that’s used by Flux, Redux, Vuex and other state management solutions.
So, we have a winner. But what about the centralized part of the deal? For that React has a long-term solution to share one state with all components: Context. According to the official documentation:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
It’s kind of a one-way, shared, centralized state that components react to when changed. Now have
useContext hook. It seems we have chosen the best tools for the work — let’s make them work together.
For this we will be using the “old and boring” increment decrement counter app. We have 3 components connected by a High Order Component (
App itself). One of them is used just to show the value of the counter, the other two are used to update the counter. Here is the code of the working app:
I have written this example in a single file for simplicity — in a real app these components would be in separate files.
Our job is to replace the props passing from the
App component to the other and keep the application functional.
First, we can make use of
useContext to replace the props on the
But, as you can check by executing the code, we lose the increment/decrement functionality, since the Context is not updatable by its consumers.
Here is where we use
useReducer. As I said before, the
useReducer function returns the state and the dispatch function, which can be used to dispatch actions that will update the state.
Inspired by the hooks that return an array with two values that can be destructured and attributed two individual variables, we’ll use
Context to share the actual state and the dispatch function.
We have to write a reducer to replace the
addToCounter function in the
One final piece that must be added is a High Order Component to make use of the
useReducer hook and set the state and dispatch as context values, since
React hooks can only be called inside components:
There we have it — our application is working again.
But what about async functions? Reducers are supposed to be pure functions, so we can’t use async await and wait for a Promise to resolve and then update the state based on the resolve result.
For that we can use something like redux-thunk, but simpler. We build a wrapping function for the
useReducer dispatch method that checks if the payload is a Promise, resolves it and then dispatches the desired action. For that we have to work on
CounterContextProvider and implement our “middleware”. We’ll implement an async that will increment the counter one second after the button is clicked.
It’s finally here! A centralized state management code that can be used to share state, and dispatch actions to update it, whether sync or assync.
You may be thinking that this project has too much code and that the solution with props is leaner — but it is just an example to show you how it can be done. Of course, in applications like this it doesn’t make much sense to use it. However, there may be occasions where your application is more complex and you don’t want to install and learn how to use 3 other packages (redux, react-redux and redux-thunk) to manage the state of the application.
One last note: this example is not meant to be the powerhouse that Redux is, nor a replacement for Redux on all projects. There are very real and deliberate limitations. For a large project that requires strong features, you should use Redux. If you have a smaller project or a team of junior developers, maybe this example would fit.