Introdução
useReducer
é um dos Ganchos adicionais que acompanham o React 16.8. Uma alternativa ao gancho useState
, ele ajuda a gerenciar a lógica de estado complexa em aplicativos React. Quando combinado com outros Ganchos como useContext
, useReducer
pode ser uma boa alternativa para Redux ou MobX-na verdade, às vezes pode ser uma opção totalmente melhor.
Isso não é para derrubar Redux e MobX, já que geralmente são as melhores opções para gerenciar o estado global em grandes aplicativos React. Porém, com mais frequência do que o necessário, muitos desenvolvedores do React pularam para essas bibliotecas de gerenciamento de estado de terceiros quando poderiam ter efetivamente tratado seu estado com Hooks.
Juntamente com a complexidade de começar com uma biblioteca de terceiros como Redux e a quantidade de código clichê necessária, gerenciar o estado com React Hooks e a API de contexto é uma opção bastante atraente, pois não há necessidade de instalar um pacote externo ou adicione vários arquivos e pastas para gerenciar o estado global em nosso aplicativo.
Mas a regra de ouro ainda permanece: estado do componente para estado do componente, Redux para estado do aplicativo.
Como funciona o useReducer
?
useReducer
é usado para armazenar e atualizar estados, assim como o gancho useState
. Ele aceita uma função redutor
como seu primeiro parâmetro e o estado inicial como o segundo.
useReducer
retorna um array que contém o valor do estado atual e uma função dispatch
, para a qual você pode passar uma ação e invocar posteriormente. É semelhante ao padrão que o Redux usa, mas com algumas diferenças.
Por exemplo, a função useReducer
é fortemente acoplada a um redutor específico. Despachamos objetos de ação apenas para aquele redutor, enquanto no Redux, a função de despacho envia o objeto de ação para a loja. No momento do envio, os componentes não precisam saber o redutor que processará a ação.
Para aqueles que não estão familiarizados com o Redux, vamos explorar esse conceito um pouco mais. Existem três blocos de construção principais no Redux:
- Uma loja-um objeto imutável que contém os dados de estado dos aplicativos
- Um redutor-uma função que retorna alguns dados de estado, disparado por uma ação
type
- Uma ação-um objeto que diz ao redutor como alterar o estado. Deve conter uma propriedade
type
e pode conter uma propriedadepayload
opcional
Vamos ver como esses blocos de construção se comparam ao estado de gerenciamento com o gancho useReducer
. Aqui está um exemplo de como é uma loja no Redux:
import {createStore} de'redux' const store=createStore (redutor, [preloadedState], [enhancer])
E aqui está como inicializar o estado com useReducer
:
const initialState={contagem: 0} const [estado, despacho]=useReducer (redutor, initialState)
A função redutora no Redux aceitará o estado anterior do aplicativo e a ação sendo despachada, calculará o próximo estado e retornará o novo objeto.
Redutores no Redux seguem esta sintaxe:
(estado=estado inicial, ação)=> novo estado
Considere o seguinte exemplo:
//observe que o estado=initialState e retorna um novo estado redutor const=(estado=estado inicial, ação)=> { switch (action.type) { case'ITEMS_REQUEST': return Object.assign ({}, state, { isLoading: action.payload.isLoading }) case ‘ITEMS_REQUEST_SUCCESS': return Object.assign ({}, state, { itens: state.items.concat (action.items), isLoading: action.isLoading }) predefinição: estado de retorno; } } exportar redutor padrão;
O React não usa o padrão Redux (state=initialState, action)=> newState
, então a função do redutor funciona um pouco diferente. Veja como você criaria redutores com React:
função redutor (estado, ação) { switch (action.type) { case'incremento': return {count: state.count + 1}; caso'decremento': return {count: state.count-1}; predefinição: lance novo Error (); } }
Agora, aqui está um exemplo de uma ação que pode ser realizada no Redux:
{tipo: ITEMS_REQUEST_SUCCESS, carga útil: {isLoading: false}} //criadores de ação export function itemsRequestSuccess (bool) { Retorna { tipo: ITEMS_REQUEST_SUCCESS, carga útil: { isLoading: bool, } } } //despachando uma ação com Redux dispatch (itemsRequestSuccess (false))//para invocar uma função de despacho, você precisa passar a ação como um argumento para a função de despacho
As ações em useReducer
funcionam de maneira semelhante:
//não é o código completo switch (action.type) { caso'incremento': return {count: state.count + 1}; predefinição: lance novo Error (); } //despachando uma ação com useReducer
Se o tipo de ação no código acima for incremento
, nosso objeto de estado é aumentado em 1.
A função redutora
O método reduce ()
em JavaScript executa uma função redutora em cada elemento da matriz an e, em seguida, retorna um único valor. O método reduce ()
aceita uma função redutora, que por si só pode aceitar até quatro argumentos. Aqui está um snippet de código para ilustrar como funciona um redutor:
redutor const=(acumulador, valor atual)=> acumulador + valor atual; [2, 4, 6, 8]. Reduzir (redutor) //saída esperada: 20
Isso é essencialmente o que acontece com useReducer
no React: ele aceita uma função redutora que retorna um único valor.
const [contagem, despacho]=useReducer (redutor, initialState);
A própria função redutora aceita dois parâmetros e retorna um valor. O primeiro parâmetro é o estado atual e o segundo é a ação. O estado são os dados que estamos manipulando. A função redutora recebe uma ação, que é executada por uma função dispatch
.
redutor de função (estado, ação) {} despachar ({tipo:'incremento'})
A ação é como uma instrução que você passa para a função redutora. Com base na ação especificada, a função do redutor executa a atualização de estado necessária. Se você usou uma biblioteca de gerenciamento de estado como o Redux, deve ter encontrado esse padrão de gerenciamento de estado.
Especificando o estado inicial
O estado inicial é o segundo argumento passado para o gancho useReducer
e representa o estado padrão.
const initialState={contagem: 1} //onde quer que nosso useReducer esteja localizado const [estado, despacho]=useReducer (redutor, initialState, initFunc)
Observe que se você não passar um terceiro argumento para useReducer
, ele receberá o segundo argumento como o estado inicial. O terceiro argumento, que é a função init
, é opcional.
Este padrão também segue uma das regras de ouro do gerenciamento de estado Redux: o estado deve ser atualizado emitindo ações. Nunca escreva diretamente para o estado.
É importante notar, no entanto, que a convenção Redux state=initialState
não funciona da mesma maneira com useReducer
. Isso ocorre porque o valor inicial às vezes depende dos adereços.
Criação do estado inicial preguiçosamente
Em programação, a inicialização preguiçosa é a tática de atrasar a criação de um objeto, o cálculo de um valor ou algum outro processo caro até a primeira vez que for necessário.
Como mencionado acima, useReducer
pode aceitar um terceiro parâmetro, que é uma função init
opcional para criar o estado inicial preguiçosamente. Ele permite que você extraia a lógica para calcular o estado inicial fora da função do redutor, como você pode ver abaixo:
const initFunc=(initialCount)=> { if (initialCount!==0) { initialCount=+ 0 } return {count: initialCount}; } //onde quer que nosso useReducer esteja localizado const [estado, despacho]=useReducer (redutor, initialCount, initFunc);
O initFunc
acima irá redefinir o initialCount
para 0
na montagem da página se o valor ainda não for 0
e, em seguida, retorne o objeto de estado. Observe que este initFunc
é uma função, não apenas uma matriz ou objeto.
O método envio
A função dispatch
aceita um objeto que representa o tipo de ação que desejamos executar quando ela é chamada. Basicamente, ele envia o tipo de ação para a função do redutor realizar seu trabalho, o que, é claro, está atualizando o estado.
A ação a ser executada é especificada em nossa função redutor, que por sua vez é passada para o useReducer
. A função redutora retornará então o estado atualizado.
As ações que serão despachadas por nossos componentes devem sempre ser representadas como um objeto com as chaves type
e payload
, onde type
está como o identificador da ação despachada e carga
é a parte da informação que esta ação adicionará ao estado.
O dispatch
é o segundo valor retornado do gancho useReducer
e pode ser usado em nosso JSX para atualizar o estado.
//criando nossa função redutora redutor de função (estado, ação) { switch (action.type) { //... case'reset': return {count: action.payload}; predefinição: lance novo Error (); } } //onde quer que nosso useReducer esteja localizado const [estado, despacho]=useReducer (redutor, initialCount, initFunc); //Atualizando o estado com a função de despacho no clique do botão
Observe como nossa função redutora usa a carga útil que é passada da função dispatch
. Ele define nosso objeto de estado para a carga útil, ou seja, qualquer que seja o initialCount
.
Digno de nota é o fato de que podemos passar a função dispatch
para outros componentes por meio de props. Este simples fato por si só é o que nos permite substituir Redux por useReducer
.
Digamos que temos um componente para o qual queremos passar nossa função de despacho como adereços. Podemos fazer isso facilmente assim a partir do componente pai:
despacho ({type:'incremento'})}/>
Agora, no componente filho, recebemos os props, que, quando emitidos, irão acionar a função de despacho e atualizar o estado:
Resgate de um despacho
Se o gancho useReducer
retornar o mesmo valor que o estado atual, o React irá resgatar sem renderizar os filhos ou disparar os efeitos. Isso ocorre porque ele usa o Object.is
algoritmo de comparação.
Construindo um aplicativo de contador simples com useReducer
Agora, vamos colocar nosso conhecimento para trabalhar criando um aplicativo de contador simples com useReducer
:
import React, {useReducer} de'react'; const initialState={contagem: 0} //A função redutora redutor de função (estado, ação) { switch (action.type) { case'incremento': return {count: state.count + 1} caso'decremento': return {count: state.count-1} case'reset': return {count: state.count=0} predefinição: return {count: state.count} } } Contador const=()=> { const [estado, despacho]=useReducer (redutor, initialState) Retorna (Contagem: {state.count}); }; Exportar contador padrão;
Primeiro, inicializamos o estado com 0
, então criamos uma função redutora que aceita o estado atual de nossa contagem como um argumento e uma ação. O estado é atualizado pelo redutor com base no tipo de ação. incremento
, decremento
e reset
são todos tipos de ação que, quando despachados, atualizam o estado de nosso aplicativo de acordo.
Portanto, para incrementar a contagem de estado const initialState={count: 0}
,
simplesmente definimos count
como state.count + 1
quando o tipo de ação incremento
é despachado.
useState
vs. useReducer
Embora useState
seja um gancho básico para gerenciar estado simples transformação e useReducer
é um Gancho adicional para gerenciar lógicas de estado mais complexas , é importante notar que useState
usa o useReducer
internamente. Isso significa que você pode usar useReducer
para tudo o que pode fazer com useState
.
No entanto, existem algumas diferenças importantes entre esses dois Ganchos. useReducer
permite que você evite passar callbacks através de diferentes níveis de seu componente, em vez de passar uma função dispatch
fornecida, que por sua vez melhorará o desempenho de componentes que acionam atualizações profundas.
Isso não implica que a função de atualização useState
seja chamada novamente em cada renderização. O que isso significa é que quando você tem uma lógica complexa para atualizar o estado, você simplesmente não usa o configurador diretamente para atualizar o estado; em vez disso, você escreverá uma função complexa, que por sua vez chamaria o configurador com o estado atualizado.
Portanto, é recomendado o uso de useReducer
, que retorna um método dispatch
que não muda entre re-renderizações, e você pode ter a lógica de manipulação no redutores.
Também é importante notar que com useState
, a função atualizador de estado é chamada para atualizar o estado, mas com useReducer
, a função dispatch
é invocado em vez disso, e uma ação com pelo menos um tipo é passada para ele.
Vamos dar uma olhada em como os dois ganchos são declarados e usados:
Declarando estado com useState
const [estado, setState]=useState ('estado padrão');
useState
retorna uma matriz que contém o valor do estado atual e um método setState
para atualizar o estado.
Declarando estado com useReducer
const [estado, despacho]=useReducer (redutor, initialState)
useReducer
retorna uma matriz que contém o valor do estado atual e um método dispatch
que logicamente atinge o mesmo objetivo de setState
, ou seja, atualizando o estado.
Atualizando estado com useState
setState (e.currentTarget.value)}/>
Atualizando estado com useReducer
Discutiremos a função dispatch
com mais detalhes um pouco mais tarde. Opcionalmente, um objeto de ação também pode ter uma carga útil
:
useReducer
pode ser útil ao gerenciar formas de estado complexas. Por exemplo, quando o estado consiste em mais do que valores primitivos, como matrizes ou objetos aninhados:
const [estado, despacho]=useReducer (loginReducer, { Comercial: [ {nome de usuário:'Philip', isOnline: false}, {nome de usuário:'Mark', isOnline: false}, {nome de usuário:'Tope', isOnline: true}, {nome de usuário:'Anita', isOnline: false}, ], carregando: falso, erro: falso, }, );
É mais fácil gerenciar este estado local porque os parâmetros dependem uns dos outros e toda a lógica pode ser encapsulada em um redutor.
Quando usar o useReducer
Hook
Assim que seu aplicativo crescer em tamanho, você provavelmente lidará com transições de estado mais complexas. Neste ponto, você ficará melhor usando useReducer
, pois ele nos dá transições de estado mais previsíveis do que useState
. Isso se torna mais importante quando as mudanças de estado se tornam tão complexas que você deseja ter um lugar (ou seja, a função de renderização) para gerenciar o estado.
Uma boa regra prática é que quando você vai além do gerenciamento de dados primitivos (ou seja, uma string, um inteiro ou booleano) e, em vez disso, deve gerenciar um objeto complexo (por exemplo, com matrizes e primitivos adicionais), você provavelmente é melhor desativado usando useReducer
.
Se você é um aprendiz mais visual, o vídeo abaixo fornece uma explicação detalhada com exemplos práticos de quando usar o gancho useReducer
.
Agora, para continuar nossa discussão, vamos criar um componente de login e comparar como gerenciaríamos o estado com os ganchos useState
e useReducer
para obter uma melhor compreensão de quando usar useReducer
.
Primeiro, o componente de login com useState
:
import React, {useState} de'react'; função padrão de exportação LoginUseState () { const [nome de usuário, setUsername]=useState (''); const [senha, setPassword]=useState (''); const [isLoading, showLoader]=useState (false); const [erro, setError]=useState (''); const [isLoggedIn, setIsLoggedIn]=useState (false); const onSubmit=async (e)=> { e.preventDefault (); setError (''); showLoader (true); tentar { aguardar login de função ({nome de usuário, senha}) { retornar nova promessa ((resolver, rejeitar)=> { setTimeout (()=> { if (nome de usuário==='ejiro'&& senha==='senha') { resolver(); } outro { rejeitar(); } }, 1000); }); } setIsLoggedIn (true); } catch (erro) { setError ('Nome de usuário ou senha incorretos!'); showLoader (false); setUsername (''); configurar senha(''); } }; Retorna (); }{isLoggedIn? ( <>Bem-vindo, {nome de usuário}!
> ): ( )}
Observe como estamos lidando com todas essas transições de estado ( nome de usuário
, senha
, isLoading
, erro
, isLoggedIn
) quando realmente deveríamos estar mais focados na ação que o usuário deseja realizar no componente de login.
Fizemos uso de cinco ganchos useState
e tivemos que nos preocupar quando cada um desses estados faz a transição. Podemos refratar o código acima para usar useReducer
e encapsular toda a nossa lógica e transições de estado em uma função redutora:
import React, {useReducer} de'react'; function loginReducer (estado, ação) { switch (action.type) { caso'campo': { Retorna { ...Estado, [action.fieldName]: action.payload, }; } case'login': { Retorna { ...Estado, erro:'', isLoading: true, }; } caso'sucesso': { Retorna { ...Estado, isLoggedIn: true, isLoading: false, }; } caso'erro': { Retorna { ...Estado, erro:'Nome de usuário ou senha incorreta!', isLoggedIn: false, isLoading: false, nome de usuário:'', senha:'', }; } case'logOut': { Retorna { ...Estado, isLoggedIn: false, }; } predefinição: estado de retorno; } } const initialState={ nome de usuário:'', senha:'', isLoading: false, erro:'', isLoggedIn: false, }; exportar função padrão LoginUseReducer () { const [estado, despacho]=useReducer (loginReducer, initialState); const {nome de usuário, senha, isLoading, erro, isLoggedIn}=estado; const onSubmit=async (e)=> { e.preventDefault (); despachar ({tipo:'login'}); tentar { aguardar login de função ({nome de usuário, senha}) { retornar nova promessa ((resolver, rejeitar)=> { setTimeout (()=> { if (nome de usuário==='ejiro'&& senha==='senha') { resolver(); } outro { rejeitar(); } }, 1000); }); } envio ({tipo:'sucesso'}); } catch (erro) { despachar ({tipo:'erro'}); } }; Retorna (); }{isLoggedIn? ( <>Bem-vindo, {nome de usuário}!
> ): ( )}
Observe como a nova implementação com useReducer
nos tornou mais focados na ação que o usuário irá realizar. Por exemplo, quando a ação login
é despachada, podemos ver claramente o que queremos que aconteça. Queremos retornar uma cópia de nosso estado atual, definir nosso erro
como string vazia e definir isLoading
como verdadeiro:
case'login': { Retorna { ...Estado, erro:'', isLoading: true, }; }
Agora, aqui está a beleza dessa implementação atual: não precisamos mais nos concentrar na transição de estado e, em vez disso, estamos interessados nas ações a serem executadas pelo usuário.
Quando não usar o useReducer
Hook
Apesar de poder usar o gancho useReducer
para lidar com a lógica de estado complexo em nosso aplicativo, é importante observar que certamente existem alguns cenários em que uma biblioteca de gerenciamento de estado de terceiros como Redux pode ser uma opção melhor . Mas como saber se React Hooks não servirá para o que você está construindo?
Uma resposta simples para essa pergunta é que você deve evitar usar Redux ou qualquer outra biblioteca de gerenciamento de estado de terceiros até que tenha problemas com o vanilla React. Se você ainda está confuso sobre se precisa, provavelmente não. Aqui estão alguns casos específicos em que faz mais sentido usar uma biblioteca como Redux ou MobX:
Quando seu aplicativo precisa de uma única fonte de verdade
Centralizar o estado e a lógica do seu aplicativo com uma biblioteca como Redux torna a criação do estado universal do aplicativo uma brisa, pois o estado do servidor pode ser facilmente serializado para o aplicativo cliente. Ter uma única fonte de verdade também permite recursos poderosos, como recursos de desfazer/refazer, que são facilmente implementados.
Quando você deseja um estado mais previsível
Usar uma biblioteca como o Redux ajuda a escrever aplicativos que se comportam de forma consistente quando executados em ambientes diferentes. Se o mesmo estado e ação são passados para um redutor, o mesmo resultado é sempre produzido porque os redutores são funções puras. Além disso, o estado no Redux é somente leitura e a única maneira de alterar o estado é emitindo uma ação, um objeto que descreve o que aconteceu.
Quando a elevação de estado para o componente de nível superior não é mais suficiente
Usar uma biblioteca como o Redux é mais adequado quando manter tudo em um estado do componente React de nível superior não é mais suficiente.
Persistência de estado
Com bibliotecas como Redux e MobX, o estado pode ser facilmente salvo em localStorage
e disponibilizado para usuários finais, mesmo após a atualização da página
Com todos esses benefícios, também é importante notar que o uso de uma biblioteca como Redux em oposição ao React puro com useReducer
traz algumas desvantagens. Redux tem uma grande curva de aprendizado e definitivamente não é a maneira mais rápida de escrever código. Em vez disso, tem como objetivo fornecer uma maneira absoluta e previsível de gerenciar o estado em seu aplicativo.
Conclusão
Acabamos de concluir nosso exame do gancho useReducer
, que é usado para gerenciar a lógica de estado complexo em nosso componente funcional React. Também vimos alguns casos de uso em que useReducer
é uma ótima opção para gerenciar a lógica de estado.
Ao construir um aplicativo React, é importante observar que nenhum Gancho pode resolver todos os nossos problemas. É tudo uma questão de entender onde cada um dos Ganchos é mais adequado, usando-os de acordo e combinando diferentes Ganchos que conseguimos gerenciar o estado em nosso aplicativo de uma forma previsível.
Como também discutimos, o gancho useReducer
nem sempre é a melhor opção. É mais importante usar o método que melhor se alinha com o tamanho e a arquitetura do seu aplicativo.
Isso é tudo por agora! Deixe-me saber na seção de comentários abaixo o que você achou deste artigo. Sou social no Twitter e no GitHub . Obrigado por ler e fique ligado.
A postagem O guia definitivo para o React useReducer
Hook apareceu primeiro no LogRocket Blog .