Gerenciando estado com ganchos de reação - sem Redux e API de contexto

Olá pessoal! Meu nome é Arthur, trabalho no VKontakte como uma equipe da web móvel, estou envolvido no projeto VKUI - uma biblioteca de componentes do React, com a ajuda da qual algumas de nossas interfaces em aplicativos móveis são escritas. A questão de trabalhar com um estado global ainda está aberta para nós. Existem várias abordagens conhecidas: Redux, MobX, Context API. Recentemente, deparei com um artigo de André Gardi State Management com React Hooks - No Redux ou Context API , no qual o autor sugere o uso do React Hooks para controlar o estado do aplicativo.

Os ganchos estão entrando rapidamente na vida dos desenvolvedores, oferecendo novas maneiras de resolver ou repensar diferentes tarefas e abordagens. Eles mudam nossa compreensão não apenas de como descrever componentes, mas também de como trabalhar com dados. Leia a tradução do artigo e o comentário do tradutor abaixo do gato.

imagem

Os ganchos de reação são mais poderosos do que você imagina


Hoje, estudaremos o React Hooks e desenvolveremos um gancho personalizado para gerenciar o estado global do aplicativo, que será mais simples que a implementação do Redux e mais produtivo que a API do Contexto.

React Hooks Basics


Você pode pular esta parte se já estiver familiarizado com os ganchos.

useState ()


Antes do aparecimento dos ganchos, os componentes funcionais não tinham a capacidade de definir um estado local. A situação mudou com o advento de useState() .



Essa chamada retorna uma matriz. Seu primeiro elemento é uma variável que fornece acesso ao valor do estado. O segundo elemento é uma função que atualiza o estado e redesenha o componente para refletir as alterações.

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


Os componentes de classe respondem a efeitos colaterais usando métodos de ciclo de vida como componentDidMount() . O gancho useEffect() permite fazer o mesmo em componentes funcionais.

Por padrão, os efeitos são acionados após cada redesenho. Mas você pode garantir que eles sejam executados somente após alterar os valores de variáveis ​​específicas, passando o segundo parâmetro opcional na forma de uma matriz.

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

Para obter um resultado semelhante ao componentDidMount() , passaremos uma matriz vazia para o segundo parâmetro. Como o conteúdo de uma matriz vazia sempre permanece inalterado, o efeito será executado apenas uma vez.

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

Partilha de Estado


Vimos que um estado de gancho funciona exatamente como um estado de componente de classe. Cada instância do componente possui seu próprio estado interno.

Para compartilhar o estado entre os componentes, criaremos nosso próprio gancho.



A idéia é criar uma matriz de ouvintes e apenas um estado. Sempre que um componente muda de estado, todos os componentes assinados chamam getState() e são atualizados devido a isso.

Podemos conseguir isso chamando useState() dentro de nosso gancho personalizado. Mas, em vez de retornar a função setState() , a adicionamos à matriz de ouvintes e retornamos uma função que atualiza internamente o objeto state e chama todos os ouvintes.

Espere um momento. Como isso facilita minha vida?


Sim, você está certo. Criei um pacote NPM que encapsula toda a lógica descrita.

Você não precisa implementá-lo em todos os projetos. Se você não deseja mais gastar tempo lendo e deseja ver o resultado final, basta adicionar este pacote ao seu aplicativo.

 npm install -s use-global-hook 

Para entender como trabalhar com um pacote, estude exemplos na documentação. E agora eu proponho me concentrar em como o pacote está organizado dentro.

Primeira versão


 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 no 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 versão já fornece o estado de compartilhamento. Você pode adicionar um número arbitrário de contadores ao seu aplicativo, e todos eles terão um estado global comum.

Mas podemos fazer melhor


O que você quer:

  • remova o ouvinte da matriz ao desmontar o componente;
  • tornar o gancho mais abstrato para usar em outros projetos;
  • gerenciar initialState usando parâmetros;
  • reescreva o gancho em um estilo mais funcional.

Chamando uma função antes de desmontar um componente


Já descobrimos que a chamada useEffect(function, []) com uma matriz vazia funciona da mesma maneira que componentDidMount() . Mas se a função passada no primeiro parâmetro retornar outra função, a segunda função será chamada imediatamente antes de desmontar o componente. Exatamente como componentWillUnmount() .

Portanto, no código da segunda função, você pode escrever a lógica para remover um componente de uma matriz de ouvintes.

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

Segunda versão


Além desta atualização, também planejamos:

  • passe o parâmetro React e livre-se da importação;
  • exportar não customHook, mas uma função que retorna customHook com o dado initalState ;
  • crie um objeto de store que conterá o valor do state e a função setState() ;
  • substitua as funções de seta pelas usuais em setState() e useCustom() para que você possa associar a store a 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; 

Separe as ações dos componentes


Se você já trabalhou com bibliotecas complexas de gerenciamento de estado, sabe que manipular um estado global a partir de componentes não é uma boa ideia.

Seria mais correto separar a lógica de negócios criando ações para alterar o estado. Portanto, desejo que a versão mais recente do pacote forneça acesso aos componentes não a setState() , mas a um conjunto de ações.

Para fazer isso, fornecemos nosso useGlobalHook(React, initialState, actions) . Só quero adicionar alguns comentários.

  • As ações terão acesso à store . Dessa forma, as ações podem ler o conteúdo de store.state , atualizar o store.setState() chamando store.setState() e até chamar outras store.actions .
  • Para evitar confusão, o objeto de ação pode conter subobjetos. Assim, você pode transferir actions.addToCounter(amount ) para um subobjeto com todas as ações do contador: actions.counter.add(amount) .

Versão final


O seguinte snippet é a versão atual do pacote 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; 

Exemplos de uso


Você não precisa mais lidar com o useGlobalHook.js . Agora você pode se concentrar em seu aplicativo. A seguir, dois exemplos de uso do pacote.

Vários contadores, um valor


Adicione quantos contadores quiser: todos eles terão um valor global. Cada vez que um dos contadores aumentará o estado global, todos os outros serão redesenhados. Nesse caso, o componente pai não precisa ser redesenhado.
Exemplo vivo .

Solicitações assíncronas de ajax


Pesquise repositórios do GitHub por nome de usuário. Processamos solicitações ajax de forma assíncrona usando async / waitit. Atualizamos o contador de consultas a cada nova pesquisa.
Exemplo vivo .

Bem, isso é tudo


Agora temos nossa própria biblioteca de gerenciamento de estado no React Hooks.

Comentário do tradutor


A maioria das soluções existentes são essencialmente bibliotecas separadas. Nesse sentido, a abordagem descrita pelo autor é interessante, pois utiliza apenas os recursos internos do React. Além disso, comparada à mesma API de contexto, que também sai da caixa, essa abordagem reduz o número de redesenhos desnecessários e, portanto, obtém desempenho.

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


All Articles