Managing State with React Hooks - Without Redux and Context API
- Transfer
Hello! My name is Arthur, I work on VKontakte as a mobile web team, I am engaged in the VKUI project - a library of React-components, with the help of which some of our interfaces in mobile applications are written. The issue of working with a global state is still open for us. There are several well-known approaches: Redux, MobX, Context API. I recently came across an article by André Gardi State Management with React Hooks - No Redux or Context API , in which the author suggests using React Hooks to control the state of the application.
Hooks are rapidly breaking into the lives of developers, offering new ways to solve or rethink different tasks and approaches. They change our understanding of not only how to describe components, but also how to work with data. Read the translation of the article and the commentary of the translator under the cat.
Today we will study React Hooks and develop a custom hook for managing the global state of the application, which will be simpler than the Redux implementation and more productive than the Context API.
You can skip this part if you are already familiar with hooks.
Before the appearance of hooks, functional components did not have the ability to set a local state. The situation has changed with the advent
This call returns an array. Its first element is a variable that provides access to the state value. The second element is a function that updates the state and redraws the component to reflect the changes.
Class components respond to side effects using lifecycle methods such as
By default, effects are triggered after each redraw. But you can make sure that they are executed only after changing the values of specific variables, passing them the second optional parameter in the form of an array.
To achieve a similar result,
We saw that a hook state works just like a class component state. Each component instance has its own internal state.
To share the state between the components, we will create our own hook.
The idea is to create an array of listeners and only one state. Every time a component changes state, all subscribed components call their own
We can achieve this by calling
Yes you are right. I created an NPM package that encapsulates all the described logic.
You do not have to implement it in every project. If you no longer want to spend time reading and want to see the final result, just add this package to your application.
To understand how to work with a package, study examples in the documentation. And now I propose to focus on how the package is arranged inside.
This version already provides sharing state. You can add an arbitrary number of counters to your application, and they will all have a common global state.
What do you want:
We already found out that calling
So, in the code of the second function, you can write the logic for removing a component from an array of listeners.
In addition to this update, we also plan:
If you've ever worked with complex state management libraries, then you know that manipulating a global state from components is not a good idea.
It would be more correct to separate the business logic by creating actions for changing the state. Therefore, I want the latest version of the package to provide components with access not to
To do this, we provide our
The following snippet is the current version of the NPM package
You no longer have to deal with
Add as many counters as you want: they will all have a global value. Each time one of the counters will increment the global state, all the others will be redrawn. In this case, the parent component does not need redrawing.
Living example .
Search GitHub repositories by username. We process ajax requests asynchronously using async / await. We update the query counter with each new search.
Living example .
We now have our own state management library on React Hooks.
Most existing solutions are essentially separate libraries. In this sense, the approach described by the author is interesting in that it uses only the built-in React features. In addition, compared to the same Context API, which also comes out of the box, this approach reduces the number of unnecessary redraws and therefore wins in performance.
Hooks are rapidly breaking into the lives of developers, offering new ways to solve or rethink different tasks and approaches. They change our understanding of not only how to describe components, but also how to work with data. Read the translation of the article and the commentary of the translator under the cat.
React Hooks are More Powerful Than You Think
Today we will study React Hooks and develop a custom hook for managing the global state of the application, which will be simpler than the Redux implementation and more productive than the Context API.
React Hooks Basics
You can skip this part if you are already familiar with hooks.
useState ()
Before the appearance of hooks, functional components did not have the ability to set a local state. The situation has changed with the advent
useState()
. This call returns an array. Its first element is a variable that provides access to the state value. The second element is a function that updates the state and redraws the component to reflect the changes.
import React, { useState } from 'react';
function Example() {
const [state, setState] = useState({counter:0});
const add1ToCounter = () => {
const newCounterValue = state.counter + 1;
setState({ counter: newCounterValue});
}
return (
You clicked {state.counter} times
);
}
useEffect ()
Class components respond to side effects using lifecycle methods such as
componentDidMount()
. A hook useEffect()
allows you to do the same in functional components. By default, effects are triggered after each redraw. But you can make sure that they are executed only after changing the values of specific variables, passing them the second optional parameter in the form of an array.
// Вызов без второго параметра
useEffect(() => {
console.log('Я буду запускаться после каждого рендера');
});
// Со вторым параметром
useEffect(() => {
console.log('Я вызовусь только при изменении valueA');
}, [valueA]);
To achieve a similar result,
componentDidMount()
we will pass an empty array to the second parameter. Since the contents of an empty array always remain unchanged, the effect will be executed only once.// Вызов с пустым массивом
useEffect(() => {
console.log('Я запущусь только первый раз');
}, []);
State sharing
We saw that a hook state works just like a class component state. Each component instance has its own internal state.
To share the state between the components, we will create our own hook.
The idea is to create an array of listeners and only one state. Every time a component changes state, all subscribed components call their own
getState()
and are updated due to this. We can achieve this by calling
useState()
inside our custom hook. But instead of returning the function setState()
, we will add it to the array of listeners and return the function that updates the state object inside and calls all the listeners.Wait a moment. How does it make my life easier?
Yes you are right. I created an NPM package that encapsulates all the described logic.
You do not have to implement it in every project. If you no longer want to spend time reading and want to see the final result, just add this package to your application.
npm install -s use-global-hook
To understand how to work with a package, study examples in the documentation. And now I propose to focus on how the package is arranged inside.
First version
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;
Use in component
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 (
counter:
{globalState.counter}
);
};
export default Counter;
This version already provides sharing state. You can add an arbitrary number of counters to your application, and they will all have a common global state.
But we can do better
What do you want:
- remove the listener from the array when unmounting the component;
- make the hook more abstract to use in other projects;
- manage
initialState
using parameters; - rewrite the hook in a more functional style.
Calling a function just before unmounting a component
We already found out that calling
useEffect(function, [])
with an empty array works the same way componentDidMount()
. But if the function passed in the first parameter returns another function, then the second function will be called right before unmounting the component. Just like componentWillUnmount()
. So, in the code of the second function, you can write the logic for removing a component from an array of listeners.
const useCustom = () => {
const newListener = useState()[1];
useEffect(() => {
// Вызывается сразу после монтирования
listeners.push(newListener);
return () => {
// Вызывается прямо перед размонтированием
listeners = listeners.filter(listener => listener !== newListener);
};
}, []);
return [state, setState];
};
Second version
In addition to this update, we also plan:
- pass React parameter and get rid of import;
- export not customHook, but a function that returns customHook with the given
initalState
; - create an object
store
that will contain a valuestate
and a functionsetState()
; - replace arrow functions with ordinary ones in
setState()
anduseCustom()
so that you can associatestore
withthis
.
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.setState];
}
const useGlobalHook = (React, initialState) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
return useCustom.bind(store, React);
};
export default useGlobalHook;
Separate actions from components
If you've ever worked with complex state management libraries, then you know that manipulating a global state from components is not a good idea.
It would be more correct to separate the business logic by creating actions for changing the state. Therefore, I want the latest version of the package to provide components with access not to
setState()
, but to a set of actions. To do this, we provide our
useGlobalHook(React, initialState, actions)
third argument. Just want to add a couple of comments.- Actions will have access to
store
. In this way, actions will be able to read the contentsstore.state
, update the state with a call,store.setState()
and even call other actions by accessingstore.actions
. - To avoid mess, the action object may contain subobjects. Thus, you can move
actions.addToCounter(amount
) in a sub-object with all ekshenom counter:actions.counter.add(amount)
.
Final version
The following snippet is the current version of 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;
Examples of using
You no longer have to deal with
useGlobalHook.js
. Now you can focus on your application. The following are two examples of using the package.Multiple Counters, One Value
Add as many counters as you want: they will all have a global value. Each time one of the counters will increment the global state, all the others will be redrawn. In this case, the parent component does not need redrawing.
Living example .
Asynchronous ajax requests
Search GitHub repositories by username. We process ajax requests asynchronously using async / await. We update the query counter with each new search.
Living example .
That's it
We now have our own state management library on React Hooks.
Translator Commentary
Most existing solutions are essentially separate libraries. In this sense, the approach described by the author is interesting in that it uses only the built-in React features. In addition, compared to the same Context API, which also comes out of the box, this approach reduces the number of unnecessary redraws and therefore wins in performance.