Managing State with useReducer, useState and useContext, is Redux dead?
The React Hooks API includes two hooks for state management: useReducer and useState. Most of the time we can manage our state using only the useState hook but in other cases the useReducer hook can be very helpful to manage complex state.
When to implement useReducer?
We usually use the useState hook for simple state management, for example when a component only has few state variables. And the useReducer hook is recommended when you have more complex state and state transitions, for example multiple state variables (usually objects and arrays) and multiple updates to those state values (multiple setState() calls).
For example this component would be a good candidate to implement useReducer:
1 2 3 4 5 6 7 8 9 10 | const restart = () => { setAnswers([]); setCurrentAnswer(''); setCurrentQuestion(0); setShowResults(false); }; |
Notice in this function we’re updating multiple state variables in sequence, something we could do with just once call using the useReducer hook:
1 2 3 4 5 6 7 | const restart = () => { dispatch({type: RESET_QUIZ}); }; |
And where is that dispatch function coming from you may ask. The useReducer hook takes two arguments a reducer function and an initial state variable and then returns a new state and a dispatch function, for example:
1 2 3 4 5 | const [state, dispatch] = useReducer(quizReducer, initialState); |
With this dispatch function we can trigger all the different actions defined in our reducer function. The dispatch function takes an object, and this object usually has a type and a payload which is basically a new value for the current state, but sometimes there’s no payload as we saw in the previous example.
But, what’s a reducer function? A reducer function or simply a reducer is a function that takes a state variable and an action and returns a new state based on that action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function quizReducer(state, action) { switch (action.type) { case RESET_QUIZ: return { ...state, currentQuestion: 0, currentAnswer: '', answers: [], showResults: false, error: '', }; default: return state; } } |
The action is usually an object with a type property and based on that type value the reducer function creates a new state. In the previous example when the type is RESET_QUIZ the function sets the values of multiple state variables: currentQuestion, currentAnswer, answers, showResults and error, all at the same time.
Also you can notice we use the Javascript spread operator (the three dots in “…state”) to return the new state object, this way we get all the properties of the current state object and just override the ones we want to update for the new state object. We should never update the state variable directly or in more fancier words the state should be immutable. For more info about the spread operator and ES6 syntax check out: https://carlosmafla.com/blog/10-things-to-know-about-es6-before-learning-react-js/
Why implement the useContext hook?
But there’s another important hook called useContext, it allows us to pass state and callback functions down the component tree without using props, solving the famous prop-drilling issue, and when we combine the useContext and useReducer hooks we have something very similar to Redux that can be used in applications with more complex state and many levels of components.
useContext + useReducer = Redux?
A few years ago if you were starting a React project usually Redux was included in all the starter kits even without being necessary, at that time there was no create-react-app tool, but as React evolved it’s being recommended to make a more conscious decision about the use of Redux in React projects. Now with hooks we can achieve something very similar to a Redux pattern. If you have used Redux you can instantly notice that the useReducer() hooks makes React code very similar to Redux. And the combination of useReducer and useContext hooks even more, and this pattern should be enough for most of the small/midsize projects that just want to avoid the famous prop-drilling.
But Redux offers other benefits, like time-travel debugging, middleware (Redux Thunk, Redux Saga, etc) and having only one single source of truth for the whole app state. Also remember that Redux is an independent library that can be used with other frameworks not just React so probably the use of Redux in React will be reduced but there are still use cases for Redux in React and other frameworks.
What about useContext + useReducer performance Issues?
First, let’s get something clear, most of the time you should not worry about optimizing your app to prevent multiple re-renders. React is very fast and instead of focusing on premature optimization you should be spending your precious time on other things like building features. Re-renders are most of the time cheap in React, it’s pretty much the nature of React to re-render on a state change so this is not something we should be obsessed with. If you don’t have an app doing complex calculations, graphs, or crazy animations you should be fine.
How to solve multiple re-renders and performance issues if I have any?
What’s recommended by the React team is to have separate context types, one context to pass down just state and another context for the dispatch function, the dispatch context never changes so components consuming it doesn’t need to re-render unless they need the application state.
Some developers recommend to implement useMemo for the context values but that doesn’t make any difference in my experience, instead we should wrap the return value for a component in useMemo, as is recommended by the React team, see this post for more info: https://github.com/facebook/react/issues/15156#issuecomment-474590693
Check out this tutorial if you want to see the video version of this post: