useEffect.dev
← Back to lessons

Lesson 11. Simplify state update logic with reducers 4. Bonus: reimplement Redux with contexts

I talked about Redux several times in this section because it made the reducer pattern popular to handle the global state of an application.

While the goal of useReducer is more to create local reducers (for local state), we can also use it to handle a state for a whole component hierarchy or even an entire application. In the previous lesson, we saw how contexts could help us share values between several components; if we add the reducers logic, we can implement something very close to what Redux offers.

To handle the global state, we will use a context named storeContext (referring to the notion of store in Redux, containing a reducer, an initial state…)

const storeContext = createContext()

This context should contain a local state (which will actually be used globally), but instead of using useState, let’s use useReducer so we can stay as close as possible as Redux:

const StoreProvider = ({ reducer, initialState, children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<storeContext.Provider value={{ state, dispatch }}>
{children}
</storeContext.Provider>
)
}

To consume the store context, we will provide two hooks: a first one to read the state and a second to dispatch an action.

To read the state, instead of just creating a hook returning the whole state, let’s do the same as what React-Redux offers: a hook taking as parameter a selector, i.e., a function extracting from the state the value we want.

A selector is usually very simple:

const selectPlanet = (state) => state.planet

The hook useSelector takes this selector as parameter and calls it to return the right piece of state:

const useSelector = (selector) => selector(useContext(storeContext).state)

Finally, the useDispatch hook simply returns the dispatch attribute from the context value:

const useDispatch = () => useContext(storeContext).dispatch

Our implementation is complete, and the code contains less than a dozen lines of code! Of course, it doesn’t implement all the functions that make Redux so powerful, such as middlewares to handle side effects (Redux-Thunk, Redux-Saga, etc.). But it makes you wonder if you always need Redux to keep track of a (small) global state with the reducer logic.

Using our implementation of Redux doesn’t change much from what we had previously. First, we can declare our selectors outside of the component (it is not required, but keeping all we can outside makes the code cleaner with a better separation of concerns):

const selectLoading = (state) => state.loading
const selectError = (state) => state.error
const selectPlanet = (state) => state.planet

Then in our components, instead of calling useReducer, we call our custom hooks to get the same three state attributes and the dispatch function:

const Planet = () => {
const loading = useSelector(selectLoading)
const error = useSelector(selectError)
const planet = useSelector(selectPlanet)
const dispatch = useDispatch()
// ...

Finally, in the main component of our application, we now use the provider we just created, passing it as props the reducer and the initial state:

export const App = () => {
return (
<StoreProvider reducer={reducer} initialState={initialState}>
<Planet />
</StoreProvider>
)
}

Complete code

Here is the final code for our custom Redux implementation, with components using it:

Result