📜 ⬆️ ⬇️

State control with React Hooks - without Redux and Context API

Hello! My name is Arthur, I work on VKontakte in the mobile web team, I work on the VKUI project - the library of React-components, with the help of which some of our interfaces are written in mobile applications. The issue of working with the global state is still open. There are several well-known approaches: Redux, MobX, Context API. Recently I 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 manage 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 not only of how to describe components, but also how to work with data. Read the article and interpreter commentary under the cut.

image

React Hooks are more powerful than you think.


Today, we will explore React Hooks and develop a custom hook to manage the global application state, which will be simpler than the Redux implementation and more productive than the Context API.
')

Basics of React Hooks


You can skip this part if you are already familiar with hooks.

useState ()


Before the advent of hooks, functional components did not have the ability to specify a local state. The situation has changed with the advent of 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 ( <div> <p>You clicked {state.counter} times</p> <button onClick={add1ToCounter}> Click me </button> </div> ); } 

useEffect ()


Class components respond to side effects using lifecycle methods such as componentDidMount() . The useEffect() hook allows you to do the same in functional components.

By default, effects start after every redraw. But you can make them run only after changing the values ​​of specific variables, passing them to the second optional parameter in the form of an array.

 //     useEffect(() => { console.log('     '); }); //    useEffect(() => { console.log('     valueA'); }, [valueA]); 

To achieve a result similar to componentDidMount() , we will pass an empty array with the second parameter. Since the contents of an empty array always remain the same, the effect will be executed only once.

 //     useEffect(() => { console.log('    '); }, []); 

State sharing


We have seen that the state hooks work the same way as the state of the class component. Each component instance has its own internal state.

For sharing between components, we will create our own hook.



The idea is to create an array of listeners and only one state. Each time a component changes its getState() , all subscribing 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 setState() function, we add it to the listeners array and return a function that internally updates the state object and calls all the listeners.

Wait a minute How will it make my life easier?


Yes you are right. I created an NPM package that encapsulates all the logic described.

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 the package, study the 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 ( <div> <p> counter: {globalState.counter} </p> <button type="button" onClick={add1Global}> +1 to global </button> </div> ); }; export default Counter; 

This version already provides sharing of the 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 you want:


Calling a function just before unmounting a component


We have already found that calling useEffect(function, []) with an empty array works the same way as 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 the 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:


 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 have ever worked with complex libraries for managing a state, then you know that manipulating a global state of components is not a good idea.

It would be more correct to separate the business logic by creating actions to change 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 will supply our useGlobalHook(React, initialState, actions) third argument. Just want to add a couple of comments.


Final version


The following snippet is the current NPM version of the use-global-hook package.

 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. Below are two examples of using the package.

Multiple counters, one value


Add as many counters as you want: they all have global meaning. Each time one of the counters increments the global state, all others will be redrawn. No redrawing is required for the parent component.
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 .

Well that's all


Now we have our own state management library for React Hooks.

Translator comment


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 capabilities of React. In addition, compared with the same Context API, which also comes out of the box, this approach reduces the number of unnecessary redrawing and therefore gains in performance.

Source: https://habr.com/ru/post/454348/


All Articles