Administrar el estado con React Hooks - Sin API Redux y Context

Hola a todos! Mi nombre es Arthur, trabajo en VKontakte como un equipo web móvil, estoy involucrado en el proyecto VKUI , una biblioteca de componentes React, con la ayuda de la cual se escriben algunas de nuestras interfaces en aplicaciones móviles. La cuestión de trabajar con un estado global todavía está abierta para nosotros. Existen varios enfoques bien conocidos: Redux, MobX, Context API. Recientemente me encontré con un artículo de André Gardi State Management con React Hooks - No Redux o Context API , en el que el autor sugiere usar React Hooks para controlar el estado de la aplicación.

Los ganchos están entrando rápidamente en la vida de los desarrolladores, ofreciendo nuevas formas de resolver o repensar diferentes tareas y enfoques. Cambian nuestra comprensión no solo de cómo describir componentes, sino también de cómo trabajar con datos. Lea la traducción del artículo y el comentario del traductor debajo del gato.

imagen

Los ganchos de reacción son más poderosos de lo que piensas


Hoy estudiaremos React Hooks y desarrollaremos un hook personalizado para administrar el estado global de la aplicación, que será más simple que la implementación de Redux y más productivo que la API Context.

Conceptos básicos de React Hooks


Puede omitir esta parte si ya está familiarizado con los ganchos.

useState ()


Antes de la aparición de los ganchos, los componentes funcionales no tenían la capacidad de establecer un estado local. La situación ha cambiado con la llegada de useState() .



Esta llamada devuelve una matriz. Su primer elemento es una variable que proporciona acceso al valor de estado. El segundo elemento es una función que actualiza el estado y vuelve a dibujar el componente para reflejar los cambios.

 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 ()


Los componentes de la clase responden a los efectos secundarios utilizando métodos de ciclo de vida como componentDidMount() . El gancho useEffect() permite hacer lo mismo en componentes funcionales.

Por defecto, los efectos se activan después de cada redibujado. Pero puede asegurarse de que se ejecuten solo después de cambiar los valores de variables específicas, pasándoles el segundo parámetro opcional en forma de matriz.

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

Para lograr un resultado similar a componentDidMount() , pasaremos una matriz vacía al segundo parámetro. Como el contenido de una matriz vacía siempre permanece sin cambios, el efecto se ejecutará solo una vez.

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

Estado compartido


Vimos que un estado de enlace funciona igual que un estado de componente de clase. Cada instancia de componente tiene su propio estado interno.

Para compartir el estado entre los componentes, crearemos nuestro propio gancho.



La idea es crear una variedad de oyentes y un solo estado. Cada vez que un componente cambia de estado, todos los componentes suscritos llaman a su getState() y se actualizan debido a esto.

Podemos lograr esto llamando a useState() dentro de nuestro useState() personalizado. Pero en lugar de devolver la función setState() , la agregamos a la matriz de oyentes y devolvemos una función que actualiza internamente el objeto de estado y llama a todos los oyentes.

Espera un momento ¿Cómo me facilita la vida?


Si tienes razon. Creé un paquete NPM que encapsula toda la lógica descrita.

No tiene que implementarlo en cada proyecto. Si ya no desea pasar tiempo leyendo y desea ver el resultado final, simplemente agregue este paquete a su aplicación.

 npm install -s use-global-hook 

Para comprender cómo trabajar con un paquete, estudie ejemplos en la documentación. Y ahora propongo centrarme en cómo se organiza el paquete por dentro.

Primera versión


 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; 

Uso en componente


 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; 

Esta versión ya proporciona el estado de compartir. Puede agregar un número arbitrario de contadores a su aplicación, y todos tendrán un estado global común.

Pero podemos hacerlo mejor


Que quieres

  • eliminar el oyente de la matriz al desmontar el componente;
  • hacer el gancho más abstracto para usar en otros proyectos;
  • gestionar initialState usando parámetros;
  • reescribe el gancho en un estilo más funcional.

Llamar a una función justo antes de desmontar un componente


Ya descubrimos que llamar a useEffect(function, []) con una matriz vacía funciona de la misma manera que componentDidMount() . Pero si la función pasada en el primer parámetro devuelve otra función, entonces se llamará a la segunda función justo antes de desmontar el componente. Exactamente igual que componentWillUnmount() .

Entonces, en el código de la segunda función, puede escribir la lógica para eliminar un componente de una matriz de oyentes.

 const useCustom = () => { const newListener = useState()[1]; useEffect(() => { //     listeners.push(newListener); return () => { //     listeners = listeners.filter(listener => listener !== newListener); }; }, []); return [state, setState]; }; 

Segunda versión


Además de esta actualización, también planeamos:

  • pasar el parámetro React y deshacerse de la importación;
  • export no customHook, sino una función que devuelve customHook con el initalState dado;
  • crear un objeto de store que contendrá el valor de state y la función setState() ;
  • reemplace las funciones de flecha con las habituales en setState() y useCustom() para que pueda asociar la store con 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(() => { //     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; 

Separar acciones de componentes


Si alguna vez trabajó con bibliotecas de administración de estado complejas, entonces sabe que manipular un estado global a partir de componentes no es una buena idea.

Sería más correcto separar la lógica de negocios creando acciones para cambiar el estado. Por lo tanto, quiero que la última versión del paquete proporcione acceso a los componentes no a setState() , sino a un conjunto de acciones.

Para hacer esto, proporcionamos nuestro useGlobalHook(React, initialState, actions) tercer argumento. Solo quiero agregar un par de comentarios.

  • Las acciones tendrán acceso a la store . De esta forma, las acciones pueden leer el contenido de store.state , actualizar el store.setState() llamando a store.setState() e incluso llamar a otras store.actions .
  • Para evitar problemas, el objeto de acción puede contener subobjetos. Por lo tanto, puede transferir actions.addToCounter(amount ) a un subobjeto con todas las acciones de contador: actions.counter.add(amount) .

Versión final


El siguiente fragmento es la versión actual del paquete NPM 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; 

Ejemplos de uso


Ya no tiene que lidiar con useGlobalHook.js . Ahora puede concentrarse en su aplicación. Los siguientes son dos ejemplos de uso del paquete.

Múltiples contadores, un valor


Agregue tantos contadores como desee: todos tendrán un valor global. Cada vez que uno de los contadores incremente el estado global, todos los demás serán redibujados. En este caso, el componente padre no necesita volver a dibujar.
Vivir ejemplo .

Solicitudes asincrónicas ajax


Buscar repositorios de GitHub por nombre de usuario. Procesamos solicitudes ajax de forma asincrónica usando async / await. Actualizamos el contador de consultas con cada nueva búsqueda.
Vivir ejemplo .

Bueno eso es todo


Ahora tenemos nuestra propia biblioteca de administración de estado en React Hooks.

Comentario del traductor


La mayoría de las soluciones existentes son esencialmente bibliotecas separadas. En este sentido, el enfoque descrito por el autor es interesante en el sentido de que utiliza solo las funciones integradas de React. Además, en comparación con la misma API de contexto, que también viene de fábrica, este enfoque reduce el número de redibujos innecesarios y, por lo tanto, gana en rendimiento.

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


All Articles