DEV Community

Yezy Ilomo
Yezy Ilomo

Posted on • Updated on

Global state management in React with global variables and hooks. State management doesn't have to be so hard.

Introduction

First of all I'd like to talk a little bit about state management in react. State management in react can be divided into two parts

  • Local state management
  • Global state management

Local states are used when we're dealing with states which are not shared among two or more components(i.e they are used within a single component)

Global states are used when components need to share states.

React provides a very good and simple way to manage local state(React hooks) but when it comes to global state management the options available are overwhelming. React itself provides the context API which many third party libraries for managing global state are built on top of it, but still the APIs built are not as simple and intuitive as react state hooks, let alone the cons of using the context API to manage global state which we won't be discussing in this post, but there are plenty of articles talking about it, so check them out if you want to explore deeper.

So what's new?

Today I want to introduce a different approach on managing global state in react which I think it might allow us to build simple and intuitive API for managing global state just like hooks API.

The concept of managing states comes from the concept of variables which is very basic in all programming languages. In managing state we have local and global states which corresponds to local and global variables in the concept of variables. In both concepts the purpose of global(state & variable) is to allow sharing it among entities which might be functions, classes, modules, components etc, while the purpose of local(state & variable) is to restrict its usage to the scope where it has been declared which might also be a function, a class, a module, a component etc.

So these two concepts have a lot in common, this made me ask my self a question
"What if we could be able to use global variables to store global states in react?".
So I decided to experiment it.

Show me the code

I started by writing a very simple and probably a dumb example as shown below

import React from 'react';

// use global variable to store global state
let count = 0;

function Counter(props){
    let incrementCount = (e) => {
        ++count;
        console.log(count);
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

ReactDOM.render(<Counter/>, document.querySelector("#root"));
Enter fullscreen mode Exit fullscreen mode

As you might have guessed this example renders count: 0 but if you click to increment, the value of count rendered doesn't change, but the one printed on a console changes. So why this happens despite the fact that we have only one count variable?.

Well this happens because when we click, the value of count increments(that's why it prints incremented value on a console) but the component Counter doesn't re-render to get the latest value of count.

So that's what we are missing to be able to use our global variable count to store a global state. Let's try to solve this by re-rendering our component when we update our global variable. Here we are going to use useState hook to force our component to re-render so that it gets a new value.

import React from 'react';

// use global variable to store global state
let count = 0;

function Counter(props){
    const [,setState] = useState();

    let incrementCount = (e) => {
        ++count;
        console.log(count);

        // Force component to re-render after incrementing `count`
        // This is hack but bare with me for now
        setState({});
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

ReactDOM.render(<Counter/>, document.querySelector("#root"));
Enter fullscreen mode Exit fullscreen mode

So this works, it'll basically re-render every time you click.

I know, I know this is not a good way to update a component in react but bare with me for now. We were just trying to use global variable to store global state and it just worked so let's just cerebrate this for now.

Okay now let's continue...

What if components need to share state?

Let's first refer to the purpose of global state,

"Global states are used when components need to share states".

In our previous example we have used count global state in only one component, now what if we have a second component in which we would like to use count global state too?.

Well let's try it

import React from 'react';

// use global variable to store global state
let count = 0;

function Counter1(props){
    const [,setState] = useState();

    let incrementCount = (e) => {
        ++count;
        setState({});
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

function Counter2(props){
    const [,setState] = useState();

    let incrementCount = (e) => {
        ++count;
        setState({});
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

function Counters(props){
    return (
        <>
            <Counter1/>
            <Counter2/>
        </>
    );
}

ReactDOM.render(<Counters/>, document.querySelector("#root"));
Enter fullscreen mode Exit fullscreen mode

Here we have two components Counter1 & Counter2, they are both using counter global state. But when you click the button on Counter1 it will update the value of count only on Counter1. On counter2 it will remain 0. Now when you click the button on Counter2 it updates but it jumps from zero to the last value on Counter1 plus one. If you go back to the Counter1 it does the same, jump from where it ended to the last value on Counter2 plus one.

Mmmmmmmh this is weird, what might be causing that?..

Well the reason for this is, when you click the button on Counter1 it increments the value of count but it re-renders only Counter1, since Counter1 and Counter2 doesn't share a method for re-rendering, each has its own incrementCount method which runs when the button in it is clicked.

Now when you click Counter2 the incrementCount in it runs, where it takes the value of count which is already incremented by Counter1 and increment it, then re-render, that's why the value of count jumps to the last value on Counter1 plus one. If you go back to Counter1 the same thing happen.

So the problem here is, when one component updates a global state other components sharing that global state doesn't know, the only component which knows is the one updating that global state. As a result when the global state is updated other components which share that global state won't re-render.

So how do we resolve this?....

It seems impossible at first but if you take a look carefully you will find a very simple solution.

Since the global state is shared, the solution to this would be to let the global state notify all the components(sharing it) that it has been updated so all of them need to re-render.

But for the global state to notify all components using it(subscribed to it), it must first keep track of all those components.

So to simplify the process will be as follows

  1. Create a global state(which is technically a global variable)

  2. Subscribe a component(s) to a created global state(this lets the global state keep track of all components subscribed to it)

  3. If a component wants to update a global state, it sends update request

  4. When a global state receives update request, it performs the update and notify all components subscribed to it for them to update themselves(re-render)

Here is the architectural diagram for more clarification
Architecture Diagram

You are probably already familiar with this design pattern, it's quite popular, it's called Observer Design Pattern.

With this and a little help from hooks, we'll be able to manage global state completely with global variables.

Let's start by implementing our global state

function GlobalState(initialValue) {
    this.value = initialValue;  // Actual value of a global state
    this.subscribers = [];     // List of subscribers

    this.getValue = function () {
        // Get the actual value of a global state
        return this.value;
    }

    this.setValue = function (newState) {
        // This is a method for updating a global state

        if (this.getValue() === newState) {
            // No new update
            return
        }

        this.value = newState;  // Update global state value
        this.subscribers.forEach(subscriber => {
            // Notify subscribers that the global state has changed
            subscriber(this.value);
        });
    }

    this.subscribe = function (itemToSubscribe) {
        // This is a function for subscribing to a global state
        if (this.subscribers.indexOf(itemToSubscribe) > -1) {
            // Already subsribed
            return
        }
        // Subscribe a component
        this.subscribers.push(itemToSubscribe);
    }

    this.unsubscribe = function (itemToUnsubscribe) {
        // This is a function for unsubscribing from a global state
        this.subscribers = this.subscribers.filter(
            subscriber => subscriber !== itemToUnsubscribe
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

From the implementation above, creating global state from now on will be as shown below

const count = new GlobalState(0);
// Where 0 is the initial value
Enter fullscreen mode Exit fullscreen mode

So we're done with global state implementation, to recap what we've done in GlobalState

  1. We have created a mechanism to subscribe & unsubscribe from a global state through subscribe & unsubscribe methods.

  2. We have created a mechanism to notify subscribers through setValue method when a global state is updated

  3. We have created a mechanism to obtain global state value through getValue method

Now we need to implement a mechanism to allow our components to subscribe, unsubscribe, and get the current value from GlobalState.

As stated earlier, we want our API to be simple to use and intuitive just like hooks API. So we are going to make a useState like hook but for global state.

We are going to call it useGlobalState.

Its usage will be like

const [state, setState] = useGlobalState(globalState);
Enter fullscreen mode Exit fullscreen mode

Now let's write it..

import { useState, useEffect } from 'react';


function useGlobalState(globalState) {
    const [, setState] = useState();
    const state = globalState.getValue();

    function reRender(newState) {
        // This will be called when the global state changes
        setState({});
    }

    useEffect(() => {
        // Subscribe to a global state when a component mounts
        globalState.subscribe(reRender);

        return () => {
            // Unsubscribe from a global state when a component unmounts
            globalState.unsubscribe(reRender);
        }
    })

    function setState(newState) {
        // Send update request to the global state and let it 
        // update itself
        globalState.setValue(newState);
    }

    return [State, setState];
}
Enter fullscreen mode Exit fullscreen mode

That's all we need for our hook to work. The very important part of useGlobalState hook is subscribing and unsubscribing from a global state. Note how useEffect hook is used to make sure that we clean up by unsubscribing from a global state to prevent a global state from keeping track of unmounted components.

Now let's use our hook to rewrite our example of two counters.

import React from 'react';

// using our `GlobalState`
let globalCount = new GlobalState(0);

function Counter1(props){
    // using our `useGlobalState` hook
    const [count, setCount] = useGlobalState(globalCount);

    let incrementCount = (e) => {
        setCount(count + 1)
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

function Counter2(props){
    // using our `useGlobalState` hook
    const [count, setCount] = useGlobalState(globalCount);

    let incrementCount = (e) => {
        setCount(count + 1)
    }

    return (
        <div>
            Count: {count}
            <br/>
            <button onClick={incrementCount}>Click</button>
        </div>
    );
}

function Counters(props){
    return (
        <>
            <Counter1/>
            <Counter2/>
        </>
    );
}

ReactDOM.render(<Counters/>, document.querySelector("#root"));
Enter fullscreen mode Exit fullscreen mode

You will notice that this example works perfectly fine. When Counter1 updates Counter2 get updates too and vice versa.

This means it's possible to use global variables to manage global state. As you saw, we have managed to create a very simple to use and intuitive API for managing global state, just like hooks API. We have managed to avoid using Context API at all, so no need for Providers or Consumers.

You can do a lot with this approach, things like selecting/subscribing to deeply nested global state, persisting global state to a local storage, implement key based API for managing global state, implement useReducer like for global state and many many more.

I myself wrote an entire library for managing global state with this approach it includes all those mentioned features, here is the link if you want to check it out https://github.com/yezyilomo/state-pool.

Thank you for making to this point, I would like to hear from you, what do you think of this approach?.

Top comments (12)

Collapse
 
supunkavinda profile image
Supun Kavinda

We use your state-pool in our production system and it works great. We don't have to worry about hard-to-maintain Context API. Thank you very much for the innovative work. Also, I wonder why React doesn't support this approach by default.

Also, I've created a minified version (200 bytes gziped) of state-pool for Preact (github.com/SupunKavinda/preact-glo...), with one hook, useGlobalState.

Btw, I saw that you are using immer as an dependency. Is it for the reducer?

Collapse
 
shang profile image
Shang

Hi @supunkavinda
I installed your package and tried to use it. But I keep getting this error.

TypeError: Cannot read properties of undefined (reading 'H')
at d (localhost:9000/
runFrame/bundle.js:45827:419)
at s (localhost:9000/__runFrame/bundle.j...)
at h (localhost:9000/__runFrame/bundle.j...)
at useGlobalState (localhost:9000/__runFrame/bundle.j...)
at CareerStoryApp (localhost:9000/__runFrame/bundle.j...)
at renderWithHooks (localhost:9000/__runFrame/bundle.j...)
at mountIndeterminateComponent (localhost:9000/__runFrame/bundle.j...)
at beginWork (localhost:9000/__runFrame/bundle.j...)
at HTMLUnknownElement.callCallback (localhost:9000/__runFrame/bundle.j...)
at Object.invokeGuardedCallbackDev (localhost:9000/__runFrame/bundle.j...)
at invokeGuardedCallback (localhost:9000/__runFrame/bundle.j...)
at beginWork$1 (localhost:9000/__runFrame/bundle.j...)
at performUnitOfWork (localhost:9000/__runFrame/bundle.j...)
at workLoopSync (localhost:9000/__runFrame/bundle.j...)
at performSyncWorkOnRoot (localhost:9000/__runFrame/bundle.j...)
at scheduleUpdateOnFiber (localhost:9000/__runFrame/bundle.j...)
at updateContainer (localhost:9000/__runFrame/bundle.j...)
at localhost:9000/__runFrame/bundle.j...
at unbatchedUpdates (localhost:9000/__runFrame/bundle.j...)
at legacyRenderSubtreeIntoContainer (localhost:9000/__runFrame/bundle.j...)
at Object.render (localhost:9000/__runFrame/bundle.j...)
at initializeBlock (localhost:9000/__runFrame/bundle.j...)
at ./frontend/index.js (localhost:9000/__runFrame/bundle.j...)
at webpack_require (localhost:9000/__runFrame/bundle.j...)
at runBlock (localhost:9000/__runFrame/bundle.j...)
at static.airtable.com/js/by_sha/a663...

Collapse
 
supunkavinda profile image
Supun Kavinda

Hey, I am no longer maintaining the package. Also, from my experience, global state using window variables can run into many pitfalls. Use nanostores if possible.

Thread Thread
 
shang profile image
Shang

Hi, thanks for your fast reply.
Okay, I will consider using nanostores.
But when you use state-pool package, did you run without any errors?
I get keeping this error.

"TypeError: Cannot read properties of undefined (reading 'setState')"

This error indicates store.setState("count", 0);

Can you please guide me?
Thank you!

Thread Thread
 
supunkavinda profile image
Supun Kavinda

Sorry, I haven't used it in a long while

Thread Thread
 
shang profile image
Shang

Okay anyway.
Thanks!

Collapse
 
yezyilomo profile image
Yezy Ilomo

Immer is used for object comparisons to make sure that even a small change in object property triggers re-render to all components subscribed to a related global state.

Collapse
 
mbjelac profile image
Marko Bjelac

haven't checked the lib implementation but checking whether the new value is different then the old with === doesn't work on objects (just checks the heap reference). would this be desirable?

Collapse
 
yezyilomo profile image
Yezy Ilomo

Lib implementation depends on immer to compare objects, so it works just fine.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

Don't use this in production please. It does not work perfectly with react and is not implemented correctly. React recommends useSyncExternalStore for library authors maybe you should take a look.

Collapse
 
samdasoxide profile image
Samuel Ramdas

Yezy nice post,
In your reRender(newState) function you give it a newState parameter not sure how this is used. How would go wrong if the function doesn't have that parameter?

Collapse
 
yezyilomo profile image
Yezy Ilomo

Thanks for the feedback, I haven't showed its purpose in this post so you can safely ignore it but the intention was to pass it in case you want to avoid re-rendering if the global value hasn't changed, which in that case you would compare the old value and the new value and decide weather to rerender or not, so in short its purpose is to avoid unnecessary rerenders which is something I haven't showed in this post. You can check how I've used it in my library I mentioned to accomplish that.