Replace Redux with useReducer
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 listTOGGLE_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.
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.
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.
In case an invalid action name or invalid payload is provided we get an error
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?