How to Utilize the useReducer and useContext Hooks to Mimic Redux Architecture
By: Alex Park
Tags: JavaScript, React, React Context, Redux, React Hook

Though there are multiple useReducer and useContext hooks in this application, I will be walking through the testCase reducer and how its state was shared across multiple components.

Before we start diving in, I’ll provide a high-level overview of what the testCase reducer is doing. The testCase component is the panel you see in the middle and the testCase reducer stores all the state for user-inputted render, action, and assertion statements. When we click the export button on the NavBar component, the ExportFileModal component gets triggered. Once the user clicks the save button on the modal, we useContext to access the state from the testCase reducer in order to generate the test file.

Step 1: Action Constants, Action Creators, Reducer

This part is easy: You write these the exact same way you would if you were to use the Redux library. I will only embed an abbreviated version with three actions here. You can view the full code for the testCaseActions.js or testCaseReducer.js if you want.

The only thing to note is line 4 of the testCaseReducer.js file where we createContext for the testCase . We do not want to provide it a default value, thus we pass in null as the argument. Note that you do not have to createContext in the reducer file and can createContext in the component where you use the Provider (in our case, App.jsx ). Because we had multiple contexts, our App.jsx was getting messy and chose to move the createContext calls to each respective reducer file.

Let’s look at how to share reducer state with useReducer and Context Provider in the next session.

Step 2: useReducer & Context Provider

Now that we have our testCaseReducer , testCaseState , and TestCaseContext , we want to provide it to the components that need it — the TestCase and ExportFileModal components.

If you take a look at a simplified version of the component tree for spearmint below, TestCase is a child component of the LeftPanel Container and ExportFileModal is a child component of the NavBar Container. Since App is the common parent component of LeftPanel and NavBar , we want to call useReducer in App.

Spearmint component tree diagram

Here is the code for App.jsx . Again, irrelevant code was removed, and if you want to see the full code for App, you can visit the GitHub.

useReducer takes in two arguments, the first being the reducer and the second being the initial state. When invoked, it returns an array with the first value being the actual state, and the second value being a method to update the state that we assign to new variables using array destructuring.

Next, we wrap the components that we want to share the testCase state with the TestCaseContext.Provider . You can only provide one value to a Provider. In order to avoid creating separate Contexts for the testCase and dispatchToTestCase , we wrap the two in an array.

Note that this concept is similar to Redux and how it provides the store to your top-level component and all of its children. We just have to create separate providers for each reducer because we don’t have a global store ala Redux.

Step 3: useContext

After we have the Providers set up, we can access the value that we gave to the Provider through useContext . I like to think of useContext as something similar to Redux’s connect method where the component in question now has access to state and a dispatch method to update the state.

Below we are using useContext in the TestCase component. The unabridged code can be found at this GitHub.

In line 7, we useContext . You invoke it by passing in a context that you created, which in our case is the TestCaseContext . If you wrapped the component or one of its parent components in a Context.Provider , the return value will be equal to the value that you passed into the Provider. In our case, it is the array containing testCase and dispatchToTestCase . Here, we are destructuring testStatement and statements out from testCase because we do not need access to the other values in testCase .

Let’s look at how we are dispatching an action to the reducer in line 10. All we need to do is invoke dispatchToTestCase in order to dispatch an action object (the return value of an action creator) to the reducer.

In Conclusion

At this point, you might be wondering, “With Hooks and Context available to me, do I ever need to use Redux now?”

Well, here are some things that I missed from Redux:

Time travel debugging! I’m back to console logging my reducer.

Not having a single source of truth, just multiple reducers.

Wrapping a component in a Context.Provider triggers a rerender of the wrapped component and all of its children when the provided changes (compared to Redux’s connect method automatically transform the component to a Pure.Component that prevents rerenders).

It was nice removing a lot of code via Hooks and Context and reducing the application size by not having to install the redux and react-redux libraries. I actually think I prefer Redux for complex projects over Hooks and Context for the above reasons.

Feel free to ask any follow-up questions in the comment, and I’ll try to respond as soon as I can.

Resources