DEV Community

toykotoykotoyko
toykotoykotoyko

Posted on

Taking the Reactivity out of React

What do you like about React?

Composability and JSX, sure. Unidirectional data flow, check.

Reactivity ...?

Does it really simplify anything to have components automatically update when a global state var changes?

The standard way of doing react is to have data in a store and to have view components basically listening for that data to change. Like this:

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++
        }
    }
}

function Counter(){

    //link to state

    return (
        <div>{store.counter.data}</div>
    )

}

Whenever the update function is called and store.counter.data increases, Counter will automatically rerender.

I've purposely left out the method to link to state here. It could be ContextApi, Redux, MobX, etc... The point is that you're going to be putting a line or two of code where it says "//link to state". Is it really that much easier to put that code there, instead of someplace else, say like:

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            //link to view
        }
    }
}

function Counter(){

    return (
        <div>{store.counter.data}</div>
    )

}

The overhead is pretty much equivalent. (I know what you're thinking, overhead is not the sole point, but stick with me, we'll definitely be addressing more than overhead.) In fact the overhead is actually exponentially less, when you consider that in order to do it the "//link to state" way, you've actually had to set up a context and a provider and/or a reducer and observables depending on what state management system you're using. You've had to set up an entire state management system just so React can automatically update when the state does.

And does it update economically? Predictably? Sort of. If you're using global state management out of the box in React, it rerenders every component by default all the time whenever a state variable changes. You can try to turn off certain components by using (old way) shouldComponentUpdate or (new way) React.memo, but this isn't a guarantee.

From the docs:

shouldComponentUpdate:

This method only exists as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs.

React.memo:

This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.

Both Redux and MobX do a great job of cutting down on unnecessary and unpredictable rerenders, but they come with the overhead of state management.

What if there was a way you could get rid of state management altogether and make React only rerender a component exactly when you told it to?

It's a lot easier to just tell a component to rerender yourself than it is to write thunks or get into sagas or think about the lifecycle all the time. Your code will not become spaghetti if you opt out of reactivity because you'll still have composability and unidirectional data flow. And guess what? It will be faster too! Because the diffing and churning of the Virtual Dom will be bypassed.

Let's go back to the alternate example above and flesh it out a bit and see how you would use it in a real application.

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            //link to view
        }
    }
}

function Counter(){

    return (
        <div>{store.counter.data}</div>
    )

}

The first thing to notice is that the way store is defined here, with the exception of the "//link to view" line, is the whole enchilada. It's not wrapped in anything, it's not associated with context/provider, it's not passed down via props, and you don't need to install any libraries to work with it. It's exactly what it looks like, a plain javascript object. Your components can access it as is.

Let's go ahead and add the action that will call store.counter.update

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            //link to view
        }
    }
}

function Counter(){

    return (
        <div onClick={store.counter.update}>{store.counter.data}</div>
    )

}

There's only one piece missing - how are we going to actually update Counter?

Let's add an update hook to our code that allows us to manually rerender our component.

function useUpdate(){
    const [value, setValue] = useState(0);
    return () => setValue(value => ++value); 
}

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            //link to view
        }
    }
}

function Counter(){

    return (
        <div onClick={store.counter.update}>{store.counter.data}</div>
    )

}

Let's create a way to call it:

function useUpdate(){
    const [value, setValue] = useState(0);
    return () => setValue(value => ++value); 
}

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            //link to view
        }
    }
}

function Counter(){

    Counter.update = useUpdate();

    return (
        <div onClick={store.counter.update}>{store.counter.data}</div>
    )

}

Note that React, even React with Hooks, is still just plain javascript under the hood. You can call functions within functional components by assigning them this way.

So, finally, let's replace "//link to view" with actual code.

function useUpdate(){
    const [value, setValue] = useState(0);
    return () => setValue(value => ++value); 
}

var store = {
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
            Counter.update();
        }
    }
}

function Counter(){

    Counter.update = useUpdate();

    return (
        <div onClick={store.counter.update}>{store.counter.data}</div>
    )

}

That's it. Nothing to install, nothing to manage, no wondering exactly when your component is going to update, no rerendering of components when it's not necessary.

In real world applications, I do like to have a separate actions part of the store to keep things cleaner, so in reality, the code would look like this:

function useUpdate(){
    const [value, setValue] = useState(0);
    return () => setValue(value => ++value); 
}

var store = {
    actions: {
        onCounterClick: function (){
            store.counter.update();
            Counter.update();
        }
    },
    counter: {
        data: 0,
        update: function (){
            store.counter.data++;
        }
    }
}

function Counter(){

    Counter.update = useUpdate();

    return (
        <div onClick={store.actions.onCounterClick}>{store.counter.data}</div>
    )

}

Note, actions do not have to refer only to a single component. This, for example, would be fine:

onCounterClick: function (){
    store.counter.update();
    store.someOtherDomain.doSomething();
    store.someOtherDomain.doSomethingElse();
    Counter.update();
    SomeOtherComponent.update();
}

As would this:

onCounterClick: function (){
    store.someOtherPartOfTheStore.doSomethingComponentsDependOnLikeAjaxCalls();
    store.counter.update();     
    Counter.update();
    //obviously contrived, let's just pretend the counter needs to hit an api first
}

This pattern has served me particularly well for apps that have a lot of animation, apps that need to have parts of the view update sequentially rather than all at once, and for interacting with APIs.

Happy hacking.

Top comments (0)