React State Management without Redux

Pourya Dashtegoli Pour
Strands Tech Corner
5 min readAug 14, 2019

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.

Baseball Player by Daria Komakowska

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.

React Context API flow
Figure 1: React Context API flow

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.

Global Store creation
Code 1: Global Store creation

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.

Reducers and how to combine them
Code 2: Reducers and how to combine them

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:

Adding middleware and handling async actions
Code 3: Adding middleware and handling async actions

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:

Action methods for both while using Redux and without Redux
Code 4: Action methods for both while using Redux and without Redux

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.

Custom Hooks vs HOCs
Code 5: Custom Hooks vs HOCs

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!

--

--