Sunteți pe pagina 1din 17

State Management with React Hooks

—No Redux or Context API


React Hooks are more powerful than you think.

André Gardi Follow


Apr 8 · 5 min read

Today, 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.

The basics of Hooks


If you are already familiar with React Hooks, you can skip this part.

useState()
Before Hooks, functional components had no state. Now, with the
useState() , we can do it.
It works by returning an array. The �rst 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 of the component to re�ect the new
values on the DOM.

1 import React, { useState } from 'react';


2
3 function Example() {
4 const [state, setState] = useState({counter:0});
5 const add1ToCounter = () => {
6 const newCounterValue = state.counter + 1;
7 setState({ counter: newCounterValue});
8 }
9
10 return (
11 <div>
12 <p>You clicked {state.counter} times</p>
13 <button onClick={add1ToCounter}>
14 Click me
15 </button>
16 </div>
17 );
18 }

hook.js hosted with ❤ by GitHub view raw


useE�ect()
Class components manage side e�ects using life cycle methods, like
componentDidMount() . The useEffect() function lets you perform
side e�ects in function components.

By default, e�ects run after every completed render. But, you can
choose to �re it only when certain values have changed, passing an
array of variables as a second optional parameter.

1 // Without the second parameter


2 useEffect(() => {
3 console.log('I will run after every render');
4 });
5
6 // With the second parameter
7 useEffect(() => {
8 console.log('I will run only when valueA changes');
9 }, [valueA]);

useEffect.js hosted with ❤ by GitHub view raw

To have the same result as componentDidMount() we can send an empty


array. Knowing that an empty set does never change, the e�ect will run
only once.

1 // With empty array


2 useEffect(() => {
3 console.log('I will run only once');
4 }, []);

useEffect.js hosted with ❤ by GitHub view raw

. . .

Sharing states
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 �red 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.

Wait. Isn’t this supposed to make my life easier?


Yes. I created a NPM package which encapsulates all this logic.

use-global-hook

Easy state management for react using hooks in


less than 1kb.
www.npmjs.com
You will not need to this rewrite this custom hook on every project. If
you just want to skip ahead and use the �nal 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.

The �rst version


1 import { useState, useEffect } from 'react';
2
3 let listeners = [];
4 let state = { counter: 0 };
5
6 const setState = (newState) => {
7 state = { ...state, ...newState };
8 listeners.forEach((listener) => {
9 listener(state);
10 });
11 };
12
13 const useCustom = () => {
14 const newListener = useState()[1];
15 useEffect(() => {
16 listeners.push(newListener);
17 }, []);
18 return [state, setState];
19 };
20
21 export default useCustom;

customHook.js hosted with ❤ by GitHub view raw

To use it on a component:
1 import React from 'react';
2 import useCustom from './customHook';
3
4 const Counter = () => {
5 const [globalState, setGlobalState] = useCustom();
6
7 const add1Global = () => {
8 const newCounterValue = globalState.counter + 1;
9 setGlobalState({ counter: newCounterValue });
10 };
11
12 return (
13 <div>
14 <p>
15 counter:
16 {globalState.counter}
17 </p>
18 <button type="button" onClick={add1Global}>
19 +1 to global
20 </button>
21 </div>
22 );
23 };
24
25 export default Counter;

Counter.jsx hosted with ❤ by GitHub view raw

This �rst 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.
But we can do better
What I didn’t like in this �rst version:

I want to remove the listener from the array when the component
is unmounted.

I want to make it more generic, so we can use in other projects.

I want set a initialState by parameters.

I want to use more functional oriented programming.

Calling a function just before component unmount


We learned that calling the useEffect(function,[]) , with an empty
array, has the same use as componentDidMount() . But, if the function
used in the �rst parameter returns another function, this second
function will be �red just before the component is unmounted. Exactly
like componentWillUnmount() .

This is the perfect place to remove the component from the listeners
array.

1 const useCustom = () => {


2 const newListener = useState()[1];
3 useEffect(() => {
4 // Called just after component mount
5 listeners.push(newListener);
6 return () => {
7 // Called just before the component unmount
8 listeners = listeners.filter(listener => listener
9 };
10 }, []);
11 return [state, setState];
12 };

customHook.js hosted with ❤ by GitHub view raw


The second version
Besides this last modi�cation, we are also going to:

Set React as a parameter, not importing it anymore.

Not exporting the customHook but, exporting a function that


returns a new customHook according to initialState

parameter.

Create a store object that contains the state value and the
setState() function.

Replace the arrow functions for regular functions in setState()

and useCustom() , so we can have a context to bind the store to


this .
1 function setState(newState) {
2 this.state = { ...this.state, ...newState };
3 this.listeners.forEach((listener) => {
4 listener(this.state);
5 });
6 }
7
8 function useCustom(React) {
9 const newListener = React.useState()[1];
10 React.useEffect(() => {
11 // Called just after component mount
12 this.listeners.push(newListener);
13 return () => {
14 // Called just before the component unmount
15 this.listeners = this.listeners.filter(listener
16 };
17 }, []);
18 return [this.state, this.setState];
19 }
20
21 const useGlobalHook = (React, initialState) => {
22 const store = { state: initialState, listeners: [] };
23 store.setState = setState.bind(store);
24 return useCustom.bind(store, React);
25 };
26
27 export default useGlobalHook;

useGlobalHook.js hosted with ❤ by GitHub view raw

Because we have a more generic Hook now, we have to setup it in a


store �le.

1 import React from 'react';


2 import useGlobalHook from './useGlobalHook';
3
4 const initialState = { counter: 0 };
5
6 const useGlobal = useGlobalHook(React, initialState);
7
8 export default useGlobal;

store.js hosted with ❤ by GitHub view raw

Separating actions from components


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:

Actions will have access to the 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 .

For organization, the actions object may contain other actions’


sub-objects. So, you may have an actions.addToCounter(amount)

or a sub-object with all counter actions called with


actions.counter.add(amount) .
The �nal version
The following �le is the actual �le in the NPM package use-global-

hook .
1 function setState(newState) {
2 this.state = { ...this.state, ...newState };
3 this.listeners.forEach((listener) => {
4 listener(this.state);
5 });
6 }
7
8 function useCustom(React) {
9 const newListener = React.useState()[1];
10 React.useEffect(() => {
11 this.listeners.push(newListener);
12 return () => {
13 this.listeners = this.listeners.filter(listener
14 };
15 }, []);
16 return [this.state, this.actions];
17 }
18
19 function associateActions(store, actions) {
20 const associatedActions = {};
21 Object.keys(actions).forEach((key) => {
22 if (typeof actions[key] === 'function') {
23 associatedActions[key] = actions[key].bind(null, store);
24 }
25 if (typeof actions[key] === 'object') {
26 associatedActions[key] = associateActions(store, actions[key]);
27 }
28 });
29 return associatedActions;
30 }
31
32 const useGlobalHook = (React, initialState, actions) =>
33 const store = { state: initialState, listeners: [] };
34 store.setState = setState.bind(store);
35 store.actions = associateActions(store, actions);
36 return useCustom.bind(store, React);
37 };
38
39 export default useGlobalHook;

useGlobalHook.js hosted with ❤ by GitHub view raw


Examples of use
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.

Several counters, one value


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.

This embedded content is from a site that does


not comply with the Do Not Track (DNT) setting
now enabled on your browser.

Please note, if you click through and view it


anyway, you may be tracked by the website
hosting the embed.

Click in “Open in Editor” to view the code in a new tab

Asynchronous ajax requests


Search GitHub repositories by username. Handle the ajax request
asynchronously with async/await. Update the requests counter on
every search.

This embedded content is from a site that does


not comply with the Do Not Track (DNT) setting
now enabled on your browser.

Please note, if you click through and view it


anyway, you may be tracked by the website
hosting the embed.

Click in “Open in Editor” to view the code in a new tab


And there we have it!
Our very own state management library with React Hooks!

S-ar putea să vă placă și