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.

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.
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.
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(() => {
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(() => {
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.