Sitemap
CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

A radically minimal React store — saying goodbye to Redux

--

Classical Redux State Change Propagation

I loved the isolated business logic and predictable change propagation patterns of Redux. A central state model for your app is better than the antipattern of blending your model with your render components with useState(). However, the boilerplate of Redux seems like pure overhead.

Why does it have to be this hard to cleanly manage state in a React App? It doesn’t. It’s easy to replace Redux with something radically simpler.

Update: 2024. The @watchable/store npm package offers a comprehensively documented Typescript implementation of the approach described in this article.

Two Arrow Functions

Binding to state and initiating changes to state can be achieved with just two arrow functions: a Selector and an Editor. I’ll use the classic Counter app as an illustration.

A Selector looks like this…

(state) => state.counter

An Editor looks like this…

(draft) => (draft.counter += 1)

Using these functions, you can bind state, trigger and track changes using a simple javascript object.

A minimal example

The Editor and Selector functions are demonstrated below in extracts from an example Typescript Counter App, which uses @watchable/store and @watchable/store-edit to manage state.

For something simpler still, see this minimal Javascript version which uses just @watchable/store . At the other extreme try this maximal Typescript version which goes further uses React Context to avoid prop-drilling, a similar pattern to Redux connect — where store bindings are instantly available to any component in the tree.

Editor Function

In this code extract from the maximal app, you can see how Editor functions are triggered directly by ui events.

Here the a call to edit() writes a new immutable state object (without altering the previous one) using Immer. If you don’t want to depend on Immer, the minimal javascript app demonstrates how to adopt immutable patterns to create a new state from the old one.

Selector Function

Selector functions are embedded in each React component, so it is easy to follow what part of the application state it is subscribed to…

See the full behaviour and its source at the sandbox for the Typescript version, or the minimal Javascript sandbox.

This radical simplicity is a departure from the Flux/Redux model — where we would have an action type, a structured payload definition, probably an Action creator, possibly a thunk creator, with the result sent via a dispatcher to (hopefully) line up with corresponding behaviour in a reducer and probably some middleware.

Using a minimal immutable Store, change propagation starts to look like this…

Change propagation without the Action and Reducer boilerplate

Isolated Business Logic

The internet is littered with articles that say ‘You probably don’t need Redux’, and which then recommend falling back on state management that’s completely dependent on React primitives and blended horribly with the render loop. You might not need Redux, but there are options.

As your model’s complexity grows, editors and state should probably be defined standalone, separately from React components. This leads to fully isolated, predictable and testable business logic, and secures the freedom to switch from React to React Native or Vue or Svelte or whatever the future brings

To help with isolated business logic you can use the @watchable/store-follow package to author pure state logic on top of @watchable/store , without any React dependency at all.

So how to define a store for immutable state to wire up our Editor and Selector functions?

A Quick Shortcut

I wrote and tested @watchable/store to embody the proposed approach, but it’s so simple you could write it for yourself.

A Store just holds a Javascript object as state, which you can read() and write() and notifies subscribers for every write. The edit() utility accepts Editor functions making it simple to write the state using immutable drafts.

The useSelected React binding hook just sets up a subscription to a store, then runs a selector to extract a particular part of the state, triggering a re-render when that part is not identical to the previous value.

You can pass a Store to your components however you like — the store wraps a changing state object, but the store reference never changes. The Store can be a singleton in a module you write, or be created in the parent component and passed to children in props. For deeply nested components you should probably use the React Context API to pass it around.

In detail: The useSelected React hook

A Selector function is passed to the useSelected hook to subscribe a React component following this basic signature…

 const value = useSelected(store, selector)

This retrieves your selected value from the store on each render. It also ensures your React component is re-rendered whenever the value you’ve selected is different after an edit (according to Object.is).

In detail: The Store edit method

An Editor function is passed to the Store’s edit method following this basic signature…

store.edit(editor)

Your Editor is passed a copy of the state tree to make edits.

Just like with Redux, never modifying the original state tree means when the state or a selected branch of the state is the same item as before, it is guaranteed to contain all the same values as before. Committing to immutability allows your business logic, renderers and memoizers to use ‘shallow equality checking’. They can efficiently check when changes to an item should trigger a re-render or recompute — simply whenObject.is(prevItem,nextItem)===false.

To ensure immutability, edit() relies on the Immer library used by Redux Toolkit. Your Editor is passed an Immer draft instead of the real state. You manipulate the draft in your editor function using normal javascript assignments and operations. When your function returns, Immer writes a new immutable state to align with the changes you made, re-using any unchanged parts. You can use regular javascript syntax and data structures in your editor function without thinking about immutability.

In Detail: Change Propagation

Change propagates automatically. After an edit completes, renders are triggered only for components whose selected values were changed by the edit.

And that’s it!

Over to you

Share your thoughts in the comments. I’m looking forward to finding the holes in this approach :)

What have I missed that justifies all the extra layers of Redux and friends?

--

--

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Cefn Hoile
Cefn Hoile

Written by Cefn Hoile

Roku Software Engineer https://cefn.com sculpting and supporting open source Prev: Cloud(@snyksec, @bbc, BT) Embedded(@ShrimpingIt,@vgkits,Make)

No responses yet