Replace Redux with useReducer

Maciek Wątroba
JavaScript in Plain English
3 min readMar 10, 2021

--

Photo by Pankaj Patel on Unsplash

Redux used to be my primary choice for managing global application state. I loved its simplicity, community around it, awesome developer tools. However, I also like to keep my apps’ dependencies to the minimum. With the context API and hooks (to be more specific useContext and useReducer) we can pretty easily replace basic Redux with tools that are already built-in react. While doing it we can still operate on concepts we already know, like actions, state or reduce function.

Reduce function

Let’s start with writing a reduce function. Just a reminder, a reducer function accepts existing state and an action, and returns a new state. We are going to use TypeScript to avoid type-related bugs and provider better developer experience. The example reducer specifies two actions:

  • ADD_TOOD that adds a new todo item to the list
  • TOGGLE_TODO thats toggles completed attribute of a specific todo item

We are using discriminating unions to type all possible actions. This is very useful, please note how TypeScript is able to properly narrow action type in reduce function.

It’s not just an Action, it’s AddTodoAction!

Now that we have our reduce function ready let’s wrap it in useReducer hook.

Context

To make redux store accessible in an app typically Provider component is used. In our case we are going to replace it with our own context (well, actually two contexts).

Let’s analyze the code above. useAppReducer returns state and dispatch function (just as a regular useReducer hook). We are going to use one context for providing state and a second one to provide dispatch. This is a performance optimisation, every time a context value changes all components that subscribe to that context are rerendered. Components that are only interested in dispatching events shouldn’t rerender when state changes, that’s the reason for introducing this separation.

Notice I use ReturnType helper to extract return type from useAppReducer and use them to type contexts’ values. You will shortly see why this is important.

Lastly I create two custom hooks for accessing state and dispatch function from context. In case ctx is null I throw an error (it means that context provider is not rendered and in this case I just want to fail fast). Notice how TypeScript properly infers return type of my custom hooks.

state type
dispatch type

Wire it

So to make application store available globally I wrap it around the entire application.

And this is how the app component looks like

Please notice that dispatch function is fully typed, it means I have autocompletion for both action name and its payload. Also, it means I don’t need ugly (at least in my opinion) consts like TODO_ACTIONS.ADD_TODO. TypeScript has it covered, it’s not possible to dispatch an action that does not exist.

I love when autocompletion just works

In case an invalid action name or invalid payload is provided we get an error

it must be a string

Of course state is fully typed as well.

Summary

And that would be it! As you can see, creating a simple redux-like state management solution from scratch is not that difficult. Who knows, maybe you don’t need redux in your next React project?

--

--