React Substate: A Redux Alternative With Perks

May 27, 2020
substate-code

I wrote a module called react-substate to solve one simple problem: How can you keep track of application state in a world where any component can useState?

The most common response I've gotten to this question has been:

You can use Redux.

To which I answer:

I don't like Redux. It takes a simple problem and solves it in a way that's so complex to me, it's often worse than not using it at all.

If you don't believe me, then try explaining to me how selectors work or what an asynchronous action creator is and why you'd need to use one. If you can do either of those in less than 5 minutes, then I'll humor you and let you plead your case.

For the rest of you, I'd encourage you to try React Substate. It aims to tackle a few key things as simply as I believe to be possible:

  • Consolidated application state
  • Consolidated state modification
  • Immutable state updates

The Details

In the module's simplest sense, here's what it looks like to create some application state and modify it:

import {createSubstate, createAction} from "react-substate" 

// In some global place...
const myState = createSubstate({foo: "Hey! It's some state!"})
const updateMyState = createAction((draft, payload) => {
    draft.foo = payload
})

// Then, in some component...
const [theState, dispatch] = useSubstate(myState)
// And
dispatch(updateMyState, "And now it's different!")

The basic flow is: Create state, create actions to modify that state, use hooks to read/modify the state.

Sounds and looks a lot like useDispatch, no? That's because it's pretty similar. The big difference, though, is that it reduces the passing around of "stuff" to zero. Just define state top-level and then use it where you need it.

But of course, this is just the beginning. You can define as many substates (and actions) as you want. You can make them as big or as small as you want, too. And that sizing becomes the basis for your rendering cadence as the substates are used throughout your app:

e.g. substates.js

export const substates = {
    data: createSubstate({
        foo: {
            bar: 'data1'
        },
        baz: 'data2'
    }),
    ui: {
        isPageLoading: createSubstate(false),
        commentForm: {
            text: createSubstate('Enter your comment'),
            isFormDirty: createSubstate(false)
        }
    }
}

Having a facility to manage all of this at the top-level of your application helps in a few ways. It can greatly reduce refactoring overhead since state hierarchy is managed externally from component hierarchy. It's also super simple to get acclimated with new code using React Substate because component directories can be used to convey the app's routing structure, while a separate, but equally important structure (substates and actions) conveys the transition between all possible application states.

Immutability

The last point I'll mention that sets React Substate apart from other state management libraries is that every single action you create is automatically passed through Immer to ensure that no matter how you manipulate your state from inside your actions, the result is always an immutable state change. This solves the immutability problem, but also cleans up the syntax even further.

e.g. actions.js

export const actions = {
    updateNestedValue: createAction(
        (draft, payload) => {
            draft.foo.bar = "It's an immutable change!"
        }
    )
}

If you're interested in what React Substate has to offer, I encourage you to check out the README for some additional information.

Related Posts