In this session, you’ll an understanding of the basics of using State Management with React Hooks. We are going to explore it and develop a custom Hook to manage global states — an easier to use method than Redux, and more performant than Context API.
If you are already familiar with React Hooks, you can skip this part.
Before Hooks, functional components had no state. Now, with the useState()
, we can do it.
It works by returning an array. The first item of the above array is a variable that gives access to the state value. The second item is a function that updates the State Management of the component to reflect the new values on the DOM.
import React, { useState } from 'react';
function Example() {
const [state, setState] = useState({counter:0});
const add1ToCounter = () => {
const newCounterValue = state.counter + 1;
setState({ counter: newCounterValue});
}
return (
<div>
<p>You clicked {state.counter} times</p>
<button onClick={add1ToCounter}>
Click me
</button>
</div>
);
}
Class components manage side effects using life cycle methods, like componentDidMount()
. The useEffect()
function lets you perform side effects in function components.
By default, effects run after every completed render. But, you can choose to fire it only when certain values have changed, passing an array of variables as a second optional parameter.
// Without the second parameter
useEffect(() => {
console.log('I will run after every render');
});
// With the second parameter
useEffect(() => {
console.log('I will run only when valueA changes');
}, [valueA]);
To have the same result as componentDidMount()
we can send an empty array. Knowing that an empty set does never change, the effect will run only once.
// With empty array
useEffect(() => {
console.log('I will run only once');
}, []);
We can see that Hooks states works exactly like class component states. Every instance of the component has its own state.
To work a solution which shares state between components, we will create a custom Hook.
The idea is to create an array of listeners and only one state object. Every time that one component changes the state, all subscribed components get their setState()
functions fired and get updated.
We can do that by calling useState()
inside our custom Hook. But, instead returning the setState()
function, we add it to an array of listeners and return a function which updates the state object and run all listeners functions.
Yes. I created a NPM package which encapsulates all this logic.
You will not need to this rewrite this custom hook on every project. If you just want to skip ahead and use the final solution, you can easily add it in your project by running:
npm install -s use-global-hook
You can learn how to use it by the examples in the package documentation. But, from now on, we are going to focus in how it works under the hood.
import { useState, useEffect } from 'react';
let listeners = [];
let state = { counter: 0 };
const setState = (newState) => {
state = { ...state, ...newState };
listeners.forEach((listener) => {
listener(state);
});
};
const useCustom = () => {
const newListener = useState()[1];
useEffect(() => {
listeners.push(newListener);
}, []);
return [state, setState];
};
export default useCustom;
import React from 'react';
import useCustom from './customHook';
const Counter = () => {
const [globalState, setGlobalState] = useCustom();
const add1Global = () => {
const newCounterValue = globalState.counter + 1;
setGlobalState({ counter: newCounterValue });
};
return (
<div>
<p>
counter:
{globalState.counter}
</p>
<button type="button" onClick={add1Global}>
+1 to global
</button>
</div>
);
};
export default Counter;
This first version already works sharing state. You can add as many Counter components as you want in your application and it will all have the same global state.
What I didn’t like in this first version:
initialState
by parameters.We learned that calling the useEffect(function,[])
, with an empty array, has the same use as componentDidMount()
. But, if the function used in the first parameter returns another function, this second function will be fired just before the component is unmounted. Exactly like componentWillUnmount()
.
This is the perfect place to remove the component from the listeners array.
const useCustom = () => {
const newListener = useState()[1];
useEffect(() => {
// Called just after component mount
listeners.push(newListener);
return () => {
// Called just before the component unmount
listeners = listeners.filter(listener => listener !== newListener);
};
}, []);
return [state, setState];
};
Besides this last modification, we are also going to:
initialState
parameter.store
object that contains the state
value and the setState()
function.setState()
and useCustom()
, so we can have a context to bind the store
to this
.function setState(newState) {
this.state = { ...this.state, ...newState };
this.listeners.forEach((listener) => {
listener(this.state);
});
}
function useCustom(React) {
const newListener = React.useState()[1];
React.useEffect(() => {
// Called just after component mount
this.listeners.push(newListener);
return () => {
// Called just before the component unmount
this.listeners = this.listeners.filter(listener => listener !== newListener);
};
}, []);
return [this.state, this.setState];
}
const useGlobalHook = (React, initialState) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
return useCustom.bind(store, React);
};
export default useGlobalHook;
Because we have a more generic Hook now, we have to setup it in a store file.
import React from 'react';
import useGlobalHook from './useGlobalHook';
const initialState = { counter: 0 };
const useGlobal = useGlobalHook(React, initialState);
export default useGlobal;
If you ever worked with complex state management library, you know that it is not the best idea to manipulate global state directly from the components.
The best way is to separate the business logic by creating actions which manipulate the state. For that reason I want that the last version of our solution doesn’t give component access to the setState()
function, but a set of actions.
To work that out, our useGlobalHook(React, initialState, actions)
function will receive an action
object as a third parameter. Regarding that, there are somethings that I want to add:
store
object. For that reason, actions may read the state with store.state
, write state through store.setState()
and even call other actions using state.actions
.actions.addToCounter(amount)
or a sub-object with all counter actions called with actions.counter.add(amount)
.The following file is the actual file in the NPM package use-global-hook
.
function setState(newState) {
this.state = { ...this.state, ...newState };
this.listeners.forEach((listener) => {
listener(this.state);
});
}
function useCustom(React) {
const newListener = React.useState()[1];
React.useEffect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter(listener => listener !== newListener);
};
}, []);
return [this.state, this.actions];
}
function associateActions(store, actions) {
const associatedActions = {};
Object.keys(actions).forEach((key) => {
if (typeof actions[key] === 'function') {
associatedActions[key] = actions[key].bind(null, store);
}
if (typeof actions[key] === 'object') {
associatedActions[key] = associateActions(store, actions[key]);
}
});
return associatedActions;
}
const useGlobalHook = (React, initialState, actions) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
store.actions = associateActions(store, actions);
return useCustom.bind(store, React);
};
export default useGlobalHook;
You will never need to touch the useGlobalHook.js
again. You may focus now on your application. Here are two examples of how to use it in real life.
Add as many counters as you want, it will all share the same global value. Every time one counter add 1 to the global value, all counters will render. The parent component won’t need to render again.
Click in “Open in Editor” to view the code in a new tab
Search GitHub repositories by username. Handle the ajax request asynchronously with async/await. Update the requests counter on every search.
Click in “Open in Editor” to view the code in a new tab
#reactjs #javascript #web-development