DEV Community

Cover image for You Can Definitely Use Global Variables To Manage Global State In React
Yezy Ilomo
Yezy Ilomo

Posted on

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.

Top comments (24)

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

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
 
soltrinox profile image
Dylan Rosario

Agree, redux is BLOATED.
The issue is my component need access from a non-local iFrame, and even maybe the browser JS runtime, above the Shadow Dom barrier. This would help me drop the legacy React and move to true ISOMORPHIC components.
Please contact me for some commissioned work surrounding tis package please.

Regards,
Dylan Rosario
dylan@rowzzy.com

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

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
 
soltrinox profile image
Dylan Rosario

Yezileli Ilomo,

Can you contact me , Hello I am Dylan Rosario i work in San Francisco.

I have a PAID commision for you, and this component seems promising for a specific task i need. Although out of the box lacks some specific features I would like to see, and rather than doing the work here int he USA with local devs i figured. could give you and the TEAM at NEUROTECH a chance to show me what you got.

I would like you to create a CUSTOM hook WEB COMPONENT from this state-pool LIB for me.

Specifically, i need to be able to access deep REACT-COMPONENTS state objects (inside the SHADOW DOM) directly from the browsers JS console. It should enforce to Reactive PUB/SUB standards that will trip the subscribers state listener to update the state value , thus a change handler event , resulting in propagation both Client side and via SSR interfaces.

This means i can force hydration and push state changes from the JS console at the browser level programmatically, with no mouse keyboard user input, . Which will fire off exactly as if a browser were triggered by interaction.

The component should probably provide a selector array of Objects and the respective states bound to the Objects.
Most specifically, i need to update FORM INPUT Boxes,

And YES this is a paid $$$ commision of this LIB for commercial use. FYI.... it will be distributed in a free custom ISOMORPHIC Library that i am releasing. I offer high level services, and I have other GLOBAL state solutions available to me..... but i figured that since your a young man from a location not too unsimilar to where my CEO Arvind Patel, ( he comes from Zanzibar) , So i figured i would give you a chance to makes some cash and possibly set up future dev work contracts.

Thanks In Advance. TIA,

Regards,
Dylan Rosario
dylan@rowzzy.com

Collapse
 
mwandi profile image
Mwandi

This is incredible πŸ‘

Collapse
 
yezyilomo profile image
Yezy Ilomo

Thank you.

Collapse
 
calag4n profile image
calag4n

Nice, I'll definitely use it !
Repo stared πŸ‘Œ.

Collapse
 
itays123 profile image
Itay Schechner

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

Collapse
 
yezyilomo profile image
Yezy Ilomo

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

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

Thank you..

Collapse
 
christopherkapic profile image
CK

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

Collapse
 
yezyilomo profile image
Yezy Ilomo

Thank you..

Collapse
 
shang profile image
Shang • Edited

Hi Yezy.
When I try to use your package, 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!