DEV Community

loading...
Cover image for You Can Definitely Use Global Variables To Manage Global State In React

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

Yezy Ilomo
Python & JavaScript Developer
・10 min read

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.

Discussion (19)

Collapse
pocheng profile image
po-cheng

Let me start off by saying state-pool looks amazing.

Since we began writing applications in React, we've always thought managing global state is such a chore. Concepts in things like Redux are quite convoluted (in my opinion) and is an absolute nightmare for our juniors. Evening after understanding the concepts, there is just so much boilerplate overhead you need to add to your code to make everything work smoothly. This should make state management simple again.

However I do have one question for you. Have you considered changing to Typescript? Or perhaps having some typings either included in the library or through @types? All of the production code in our company are written in Typescript and the development experience when we use a library without typings is quite horrible. I am slightly apprehensive about using this in our production code due to this reason.

Collapse
yezyilomo profile image
Yezy Ilomo Author

Am glad you found it simpler, I’ve actually used Redux and other state management libraries before and what you’re saying about complexity is very true, I think most of them are over engineered which makes it very hard for beginners to learn, Part of the reason I developed this lib was to make things easier that’s why I’ve tried my best(still do) to keep the API very minimal and very similar to builtin react state hook’s API.

Lots of people have requested TS support so we are definitely going to work on it, if things go well we might have it supported on the next release so hang in there.

Collapse
pocheng profile image
po-cheng

That's great news. Thank you for your amazing work!

Collapse
supunkavinda profile image
Supun Kavinda • Edited

We use your state-pool in our production application (quite a large one) and it's working great. Thank you for the work!

However, we have written an extended version of useGlobalState like this.

export default function useGlobalStateExtended(key, def) {
    const [x, update, set] = useGlobalState(key, {
        default: def
    });
    return [x, set];
}
Enter fullscreen mode Exit fullscreen mode

It allows us to easily define a default in the param, without adding it inside an object. And, returns only the value and set function (we are not interested in the update function). Btw, looks like you have made a change to the return value. The version we use returns [value, update, set] but in your example here it's changed to [value, set, update]. It's good move.

This is a great library to solve a lot of headaches in React applications.

Collapse
yezyilomo profile image
Yezy Ilomo Author

Glad to know that you are using state-pool in production. The change was made in v0.4.0, I think [state, setState, updateState] makes more sense than [state, updateState, setState], it was also influenced by [state, setState] from useState since we don't want state-pool's API to be very different from react state hook's API.

Collapse
arthuro555 profile image
Arthur Pacaud

Ah that is funny, it kind of reminds me of a little project i have made not long ago: github.com/arthuro555/create-proje...
Though mine is not production ready and really focused on a specific use case 😅. I'll look into using this for more general purpose global state management.

Collapse
christopherkapic profile image
Christopher Kapic

I will certainly give this a look on my next React project. Nice work!

Collapse
yezyilomo profile image
Yezy Ilomo Author

Thank you..

Collapse
calag4n profile image
calag4n

Nice, I'll definitely use it !
Repo stared 👌.

Collapse
mwandi profile image
Mwandi

This is incredible 👏

Collapse
yezyilomo profile image
Yezy Ilomo Author

Thank you.

Collapse
miketalbot profile image
Mike Talbot

I use something very similar in my projects and it's super helpful. In respect to this article - really nice docs and I love your interface.

Collapse
yezyilomo profile image
Yezy Ilomo Author

Thank you..

Collapse
itays123 profile image
Itay Schechner

Brilliant! I will sure use it in a future project someday.

Collapse
yezyilomo profile image
Yezy Ilomo Author

Thank you!, would love to hear your feedback when you do so.

Collapse
thgh profile image
Thomas Ghysels

Consider a comparison with zustand, which does exactly the same.
This approach does not work well with SSR and I would actually not recommend it.

Collapse
yezyilomo profile image
Yezy Ilomo Author

It actually works really well with SSR, I’ve used it several times with Nextjs, Stay tuned my next post will be about SSR with state-pool.

Collapse
arash16 profile image
Arash Shakery

An what about SSR? This methodoly promotes so many bad patterns, developing non-reusable components being one of them.

Collapse
yezyilomo profile image
Yezy Ilomo Author

dev.to/yezyilomo/comment/1g7na, Would you mention them please?, non-reusable components how?.