You Can Definitely Use Global Variables To Manage Global State In React

yezyilomo

Yezy Ilomo

Posted on July 7, 2021

You Can Definitely Use Global Variables To Manage Global State In React

Introduction

React provides a very good and simple way to manage local states through state hooks but when it comes to global states 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 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 managing global states in react is still a problem with no clear solution yet.

But what if I tell you there might be a solution which is based on global variables?

Yes the global variables that you use every day in your code.


How is it possible?

The concept of managing states is very similar to the concept of variables which is very basic in almost all programming languages.

In state management we have local and global states which corresponds to local and global variables in a concept of variables.

In both concepts the purpose of global(state & variable) is to allow sharing of a value 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 myself a question

"What if we use global variables to store global states in react?".


Answers

As of now we can use a normal global variable to store a global state but the problem comes when we want to update it.

If we use regular global variable to store react global state we won't be able to get the latest value of our state right away when it gets updated, because there's no way for react to know if a global variable has changed for it to re-render all components depending on such global variable in order for them to get a fresh(updated) value. Below is an example showing this problem



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 initially 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 the button is clicked, 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 this is the only problem standing in our way to use global variables to manage global state in react.


Solution

Since global states are shared among components, the solution to our problem would be to let a global state notify all the components which depend on it that it has been updated so that all of them re-render to get a fresh value.

But for the global state to notify all components using it(subscribed to it), it must first keep track of 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) to get a fresh value

Here is the architectural diagram for visual clarification
Architecture Diagram

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

Luckily we won't need to implement this on ourselves because State Pool got our back.


Introducing State Pool✨🎉.

State Pool is a react state management library based on global variables and react hooks. Its API is as simple and intuitive as react state hooks, so if you have ever used react state hooks(useState or useReducer) you will feel so familiar using state-pool. You could say state-pool is a global version of react state hooks.

Features and advantages of using State Pool

  • Simple, familiar and very minimal core API but powerful
  • Built-in state persistence
  • Very easy to learn because its API is very similar to react state hook's API
  • Support selecting deeply nested state
  • Support creating global state dynamically
  • Support both key based and non-key based global state
  • States are stored as global variables(Can be used anywhere)


Installing

yarn add state-pool

Or

npm install state-pool


Getting Started

Now let's see a simple example of how to use state-pool to manage global state



import React from 'react';
import {store, useGlobalState} from 'state-pool';


store.setState("count", 0);

function ClicksCounter(props){
    const [count, setCount] = useGlobalState("count");

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

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

ReactDOM.render(ClicksCounter, document.querySelector("#root"));


Enter fullscreen mode Exit fullscreen mode

If you've ever used useState react hook the above example should be very familiar,

Let's break it down

  • On a 2nd line we're importing store and useGlobalState from state-pool.

  • We are going to use store to keep our global states, so store is simply a container for global states.

  • We are also going to use useGlobalState to hook in global states into our components.

  • On a 3rd line store.setState("count", 0) is used to create a global state named "count" and assign 0 as its initial value.

  • On 5th line const [count, setCount] = useGlobalState("count") is used to hook in the global state named "count"(The one we've created on 3rd line) into ClicksCounter component.

As you can see useGlobalState is very similar to useState in so many ways.


Updating Nested Global State

State Pool is shipped with a very good way to handle global state update in addition to setState especially when you are dealing with nested global states.

Let's see an example with nested global state



import React from 'react';
import {store, useGlobalState} from 'state-pool';


store.setState("user", {name: "Yezy", age: 25});

function UserInfo(props){
    const [user, setUser, updateUser] = useGlobalState("user");

    let updateName = (e) => {
        updateUser(function(user){
            user.name = e.target.value;
        });
    }

    return (
        <div>
            Name: {user.name}
            <br/>
            <input type="text" value={user.name} onChange={updateName}/>
        </div>
    );
}

ReactDOM.render(UserInfo, document.querySelector("#root"));


Enter fullscreen mode Exit fullscreen mode

In this example everything is the same as in the previous example

On a 3rd line we're creating a global state named "user" and set {name: "Yezy", age: 25} as its initial value.

On 5th line we're using useGlobalState to hook in the global state named "user"(The one we've created on a 3rd line) into UserInfo component.

However here we have one more function returned in addition to setUser which is updateUser, This function is used for updating user object rather than setting it, though you can use it to set user object too.

So here updateUser is used to update user object, it's a higher order function which accepts another function for updating user as an argument(this another functions takes user(old state) as the argument).

So to update any nested value on user you can simply do



updateUser(function(user){
    user.name = "Yezy Ilomo";
    user.age = 26;
})


Enter fullscreen mode Exit fullscreen mode

You can also return new state instead of changing it i.e



updateUser(function(user){
    return {
        name: "Yezy Ilomo",
        age: 26
    }
})


Enter fullscreen mode Exit fullscreen mode

So the array returned by useGlobalState is in this form [state, setState, updateState]

  • state hold the value for a global state
  • setState is used for setting global state
  • updateState is used for updating global state


Selecting Nested State

Sometimes you might have a nested global state but some components need to use part of it(nested or derived value and not the whole global state).

For example in the previous example we had a global state named "user" with the value {name: "Yezy", age: 25} but in a component UserInfo we only used/needed user.name.

With the approach we've used previously the component UserInfo will be re-rendering even if user.age changes which is not good for performance.

State Pool allows us to select and subscribe to nested or derived states to avoid unnecessary re-renders of components which depends on that nested or derived part of a global state.

Below is an example showing how to select nested global state.



import React from 'react';
import {store, useGlobalState} from 'state-pool';


store.setState("user", {name: "Yezy", age: 25});

function UserInfo(props){
    const selector = (user) => user.name;  // Subscribe to user.name only
    const patcher = (user, name) => {user.name = name};  // Update user.name

    const [name, setName] = useGlobalState("user", {selector: selector, patcher: patcher});

    let handleNameChange = (e) => {
        setName(e.target.value);
    }

    return (
        <div>
            Name: {name}
            <br/>
            <input type="text" value={name} onChange={handleNameChange}/>
        </div>
    );
}

ReactDOM.render(UserInfo, document.querySelector("#root"));


Enter fullscreen mode Exit fullscreen mode

By now from an example above everything should be familiar except for the part where we pass selector and patcher to useGlobalState hook.

To make it clear, useGlobalState accept a second optional argument which is the configuration object. selector and patcher are among of configurations available.

  • selector: should be a function which takes one parameter which is the global state and returns a selected value. The purpose of this is to subscribe to a deeply nested state.

  • patcher: should be a function which takes two parameters, the first one is a global state and the second one is the selected value. The purpose of this is to merge back the selected value to the global state once it's updated.

So now even if user.age changes, the component UserInfo won't re-render because it only depend on user.name


Creating Global State Dynamically

State Pool allows creating global state dynamically, this comes in handy if the name or value of a global state depend on a certain parameter within a component(it could be server data or something else).

As stated earlier useGlobalState accepts a second optional parameter which is a configuration object, default is one of available configurations.

default configuration is used to specify the default value if you want useGlobalState to create a global state if it doesn't find the one for the key specified in the first argument. For example



const [user, setUser, updateUser] = useGlobalState("user", {default: null});


Enter fullscreen mode Exit fullscreen mode

This piece of code means get the global state for the key "user" if it's not available in a store, create one and assign it the value null.

This piece of code will work even if you have not created the global state named "user", it will just create one if it doesn't find it and assign it the default value null as you have specified.


useGlobalStateReducer

useGlobalStateReducer works just like useReducer hook but it accepts a reducer and a global state or key(name) for the global state. In addition to the two parameters mentioned it also accepts other optional parameter which is the configuration object, just like in useGlobalState available configurations are selector, patcher, default and persist(This will be discussed later). For example if you have a store setup like



const user = {
    name: "Yezy",
    age: 25,
    email: "yezy@me.com"
}

store.setState("user": user);


Enter fullscreen mode Exit fullscreen mode

You could use useGlobalStateReducer hook to get global state in a functional component like



function myReducer(state, action){
    // This could be any reducer
    // Do whatever you want to do here
    return newState;
}

const [name, dispatch] = useGlobalStateReducer(myReducer, "user");


Enter fullscreen mode Exit fullscreen mode

As you can see, everthing here works just like in useReducer hook, so if you know useReducer this should be familiar.

Below is the signature for useGlobalStateReducer



useGlobalStateReducer(reducer: Function, globalState|key: GlobalState|String, {default: Any, persist: Boolean, selector: Function, patcher: Function})


Enter fullscreen mode Exit fullscreen mode


State Persistance

Sometimes you might want to save your global states in local storage probably because you might not want to lose them when the application is closed(i.e you want to retain them when the application starts).

State Pool makes it very easy to save your global states in local storage, all you need to do is use persist configuration to tell state-pool to save your global state in local storage when creating your global state.

No need to worry about updating or loading your global states, state-pool has already handled that for you so that you can focus on using your states.

store.setState accept a third optional parameter which is the configuration object, persist is a configuration which is used to tell state-pool whether to save your state in local storage or not. i.e



store.setState(key: String, initialState: Any, {persist: Boolean})


Enter fullscreen mode Exit fullscreen mode

Since state-pool allows you to create global state dynamically, it also allows you to save those newly created states in local storage if you want, that's why both useGlobalState and useGlobalStateReducer accepts persist configuration too which just like in store.setState it's used to tell state-pool whether to save your newly created state in local storage or not. i.e



useGlobalState(key: String, {defaultValue: Any, persist: Boolean})


Enter fullscreen mode Exit fullscreen mode


useGlobalStateReducer(reducer: Function, key: String, {defaultValue: Any, persist: Boolean})


Enter fullscreen mode Exit fullscreen mode

By default the value of persist in all cases is false(which means it doesn't save global states to the local storage), so if you want to activate it, set it to be true. What's even better about state-pool is that you get the freedom to choose what to save in local storage and what's not to, so you don't need to save the whole store in local storage.

When storing state to local storage, localStorage.setItem should not be called too often because it triggers the expensive JSON.stringify operation to serialize global state in order to save it to the local storage.

Knowing this state-pool comes with store.LOCAL_STORAGE_UPDATE_DEBOUNCE_TIME which is the variable used to set debounce time for updating state to the local storage when global state changes. The default value is 1000 ms which is equal to 1 second. You can set your values if you don't want to use the default one.


Non-Key Based Global State

State Pool doesn't force you to use key based global states, if you don't want to use store to keep your global states the choice is yours

Below are examples showing how to use non-key based global states



// Example 1.
import React from 'react';
import {createGlobalState, useGlobalState} from 'state-pool';


let count = createGlobalState(0);

function ClicksCounter(props){
    const [count, setCount, updateCount] = useGlobalState(count);

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

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

ReactDOM.render(ClicksCounter, document.querySelector("#root"));


Enter fullscreen mode Exit fullscreen mode




// Example 2
const initialGlobalState = {
    name: "Yezy",
    age: 25,
    email: "yezy@me.com"
}

let user = createGlobalState(initialGlobalState);


function UserName(props){
    const selector = (user) => user.name;  // Subscribe to user.name only
    const patcher = (user, name) => {user.name = name};  // Update user.name

    const [name, setName, updateName] = useGlobalState(user, {selector: selector, patcher: patcher});

    let handleNameChange = (e) => {
        setName(e.target.value);
        // updateName(name => e.target.value);  You can do this if you like to use `updatName`
    }

    return (
        <div>
            Name: {name}
            <br/>
            <input type="text" value={name} onChange={handleNameChange}/>
        </div>
    );
}


Enter fullscreen mode Exit fullscreen mode


Conclusion

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

If you liked the library give it a star at https://github.com/yezyilomo/state-pool.

💖 💪 🙅 🚩
yezyilomo
Yezy Ilomo

Posted on July 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related