You Can Definitely Use Global Variables To Manage Global State In React
Yezy Ilomo
Posted on July 7, 2021
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"));
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
Create a global state(which is technically a global variable)
Subscribe a component(s) to a created global state(this lets the global state keep track of all components subscribed to it)
If a component wants to update a global state, it sends update request
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
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"));
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
anduseGlobalState
fromstate-pool
.We are going to use
store
to keep our global states, sostore
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) intoClicksCounter
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"));
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;
})
You can also return new state instead of changing it i.e
updateUser(function(user){
return {
name: "Yezy Ilomo",
age: 26
}
})
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"));
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});
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);
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");
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})
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})
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})
useGlobalStateReducer(reducer: Function, key: String, {defaultValue: Any, persist: Boolean})
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"));
// 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>
);
}
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.
Posted on July 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.