React State Management without Redux
Mimicking Redux features with React Hooks and Context API
In the React world, one of the first ideas that come to mind when thinking about state management is using the Redux library.
Redux provides a predictable state container to centralize the global store of your application. Besides, with features like store, reducers, actions and middlewares, and methods such asconnect
, mapStateToProps
, mapDispatchToProps
and bindActionsCreator
, Redux allows to easily create and manage a complex application.
Since the stable release of React Hooks (16.8v), we can natively implement our state management using only React goodies without any hassle.
In this post, we are going to identify the main features of Redux and we will try to replace them, using React Hooks and Context API, to create an app with “Redux-like” state management.
Global Store
Redux provides a global store, which allows the different components of an application to share data. In order to do so, all you have to do is pass your root reducer to the createStore
method, then access the global store and wrap your App component in a Provider from react-redux
, passing the store as its prop. Redux is now plugged into your app and ready to go.
Implementing this part is easy with a nice feature called Context API, which was released way before Hooks and has been empowered by Hooks API’s useContext
.
If you have been around long enough, you might be familiar with Context API (React 16.3v,) which centralizes and supplies global data.
Creating Context Providers on the top level and instantiating Context Consumers every time you had to retrieve data from your global store used to be a dull chore. However, React Hooks API allows us to carry out this task in a much quicker and effective manner.
Below, you can see how we can create our global store using React’s new features and Redux. You will notice that both ways are almost identical and very easy to perform.
From here, we can export the GlobalStore
so other parts of the application can use it. The code below is a little custom Hook we can add to our store file to pass state and dispatch function of the current GlobalStore
:
export const useGlobalStore = () => React.useContext(GlobalStore)
Reducer
In the previous line of code, we saw how to handle the global store using Context and Hooks API. However, as an application gets bigger, so does the number of its reducers, and we definitely cannot handle many actions with just one reducer. Next, we will compare the way we create reducers and how we would combine them with both Redux and Hooks.
Actions
With React, managing the actions corresponding to each reducer is just as simple as with Redux, although it is important to consider both synchronous and asynchronous action handlings. In Redux, the usual way to orchestrate asynchronous actions is using redux-thunk
, redux-promise
or some sort of middleware.
First, let’s see how to manage middlewares without Redux:
As the code above shows, we can achieve this by implementing our middleware and then updating our dispatch function. To do this, we will use one of the new React Hooks methods called useCallback
.
useCallback
will return a memoized version of the callback that will only change if one of the dependencies changes. useCallback
will receive two parameters — one callback function and an array of dependencies — and it will return a memoized version of the callback; by passing an empty array as the second parameter, the callback will be returned after each change, which makes it suitable for our dispatch function to be updated on every state change.
Also, asyncer
is a little method that can almost do what redux-thunk
does to manage asynchronous actions. We can use methods like asyncer
as middlewares to affect our dispatch
method before passing it to components.
From here, we can pass both async and sync actions to our dispatch function as follows:
Let’s use Hooks to replace HOCs
The greatest strength of Hooks API, in my opinion, is the ability to write your own Hook.
Building your own Hooks lets you extract component logic into reusable functions.
In this segment, we are going to see the differences between using higher-order components and custom Hooks to inject state and actions into app components. As a matter of fact, a higher-order Component (HOC) is not a Redux thing, but a compositional design pattern used in React apps to create a component with a new set of props needed across the application. We often implement HOCs using Redux’s connect
method to compose a reusable component.
Conclusion
As we discussed, implementing state management using only React is easily achievable. By using React’s native features like Context and Hooks API, we can write more robust apps with fewer lines of code, fewer dependencies and an optimized flow of data. However, this also has some cons that should be kept in mind, like not being able to benefit from Redux’s awesome dev tools, its huge community and the great set of features and ready-to-use middlewares that are delivered with it.
We hope you enjoyed this post. If you did, please go ahead and check out the full source code of the little app above (here on Github), which has all the great features of React v16.8 in Typescript we just talked about.
Cheers!