Are new React APIs replacing Redux? (Part 2)

Disclaimer: this post series is discussing alpha features of React. A lot of
this is up in the air and is being actively discussed by the React team and the
community. The features, problems, solutions, opinions and decisions that these
posts discuss might not be final. This is as a summary of the trade-offs being
discovered and discussed. I find these behind the scenes discussions to be
interesting and educational. Writing these posts is my way of understanding the
topics in more depth. Finally, the content is interleaved with with my personal
experience and opinions. I hope you enjoy!

This post is Part 2 of a series of posts looking at the new React APIs —Hooks
and Context and how they compare to Redux. Make sure to check out Part
1.

Now that React has a few new primitives, could we and should we avoid using
Redux altogether? The useContext hook is “live” now, it observes the changes
to the context value and rerenders the consuming components. This is great for
reacting to state changes. The useReducer can be used for updating state in a
robust manner by dispatching actions.

Can these be combined to avoid the need for external libraries? Yes and no.

As I’ve mentioned in the previous post, I really like the direction the React
team is taking, Hooks are an amazing, expressive way of writing React
components. Having these new primitives such as useContext or useReducer is
great for simpler applications and components. These primitives are also great
building blocks for more specialised use cases.

I find using Redux like central stores for storing application data and/or state
very convenient. It’s a bit like creating a client side database for your
application that you can query for data and update with actions. So can you
create a central store using new React APIs?

In principle, yes, you can combine useContext and useReducer APIs to create
your own Redux like store. Many people blogged and tweeted about this, here’s
one example of such implementation:

For over two years I’ve been working on a store similar to Redux –
tiny-atom. By building several apps
with largish state trees, many state transitions, fairly intricate UIs and user
interactions, we’ve found the following to be important requirements for optimal
performance:

Avoid rerendering the app on each state change. Instead batch the changes and
rerender at most once per frame/tick by default.

Avoid rerendering children components twice if both parent and children
components need to rerender because of a state change.

Avoid rerendering components if unrelated parts of state change. Only rerender
if the change is in the mapped state.

Our simple store implementation above does OK on these requirements, but it’s
not perfect.

Our implementation batches rapid state changes and rerenders only once if the
dispatch originates in a user event handler. If the dispatch happens outside of
an event, for example in a fetch callback, the rerenders are not batched.

It does well on the second requiremnt. It doesn’t render children components
multiple times, even if both parent and children components are subscribed to
the store using the useStore hook. That’s because React’s Context ensures the
top down render order and only a single pass.

The third requirement is missed entirely. There is no ability in our
implementation to subscribe to slices of state to avoid rerendering the entire
application on each state change. That is largely what discussion on
facebook/react#14110 is about.

But is that such a bad thing? My current thinking is that state management
libraries like Redux, tiny-atom, react-refetch or react-apollo give you quite a
bit more functionality, more expressive APIs and it’s ok to use them. These
libraries can combine React primitives in more complex ways to achieve their own
goals. React itself does not necessarily need to get any more complicated.

Use a ref to store previously mapped state and use it for diffing later

Use an effect to subscribe to store changes

Debounce each store change into at most once per frame

When store changes, map the state and diff against previous mapped state

If state is different, update the local state, which is what causes rerender

If parent rerenders the component, cancel the child’s scheduled update

This implementation satisfies all of the 3 requirements we stated earlier.

I’d be very interested to hear feedback on this implementation (ping me on
Twitter). In particular:

I’m keeping track of rendering order in a ref. This is so that I could push
store subscriptions in order that the components were rendered, because it’s
important to always rerender parents before rerendering children. Think of a
modal that is conditionally rendered by App and also renders some data from the
store. If you remove that data, the App should rerender first to remove the
modal, or else the modal might throw an exception when it’s not able to find the
data in the store. Is keeping track of rendering order in a ref safe? Is there a
better way to achieve this? If you subscribe using useEffect, the children
subscrib first, so that doesn’t work. Update: there’s another, possibly
better way to achieve this using the unstable_batchedUpdates function.

Will this implementation fail in React’s concurrent mode, because of how it
always reads the latest state of the store? I’m wondering if something bad could
happen when store changes, component rerenders, concurrent react interupts and
dismisses the result, store changes again, react resumes rendering component,
but the store now has a different value?

In Part
3
we explore how Redux used the new React APIs internally, what issues it ran into
and whether Redux is a good approach for UI development to begin with.

Read Next

Disclaimer: this post series is discussing alpha features of React. A lot of
this is up in the air and is being actively discussed by the React team and the
community. The features, problems, solutions, opinions and decisions that these
posts discuss might not be final. This is… Continue →