v19.2Latest

Extraindo a Lógica de Estado para um Redutor

Componentes com muitas atualizações de estado espalhadas por vários manipuladores de eventos podem ficar confusos. Para esses casos, você pode consolidar toda a lógica de atualização de estado fora do seu componente em uma única função, chamada deredutor.

Você aprenderá
  • O que é uma função redutora
  • Como refatoraruseStateparauseReducer
  • Quando usar um redutor
  • Como escrever um bem

Consolidar a lógica de estado com um redutor

À medida que seus componentes crescem em complexidade, pode ficar mais difícil ver de relance todas as diferentes maneiras pelas quais o estado de um componente é atualizado. Por exemplo, o componenteTaskAppabaixo mantém um array detasksno estado e usa três manipuladores de eventos diferentes para adicionar, remover e editar tarefas:

Cada um de seus manipuladores de eventos chamasetTaskspara atualizar o estado. À medida que este componente cresce, também cresce a quantidade de lógica de estado espalhada por ele. Para reduzir essa complexidade e manter toda a sua lógica em um lugar de fácil acesso, você pode mover essa lógica de estado para uma única função fora do seu componente,chamada de “redutor”.

Redutores são uma maneira diferente de lidar com o estado. Você pode migrar deuseStateparauseReducerem três etapas:

  1. Moverde definir estado para despachar ações.
  2. Escreveruma função redutora.
  3. Usaro redutor do seu componente.

Etapa 1: Mover de definir estado para despachar ações

Seus manipuladores de eventos atualmente especificamo que fazerdefinindo o estado:

Remova toda a lógica de definição de estado. O que resta são três manipuladores de eventos:

  • handleAddTask(text)é chamado quando o usuário pressiona “Adicionar”.
  • handleChangeTask(task)é chamado quando o usuário alterna uma tarefa ou pressiona “Salvar”.
  • handleDeleteTask(taskId)é chamado quando o usuário pressiona “Excluir”.

Gerenciar estado com redutores é ligeiramente diferente de definir estado diretamente. Em vez de dizer ao React “o que fazer” definindo o estado, você especifica “o que o usuário acabou de fazer” despachando “ações” dos seus manipuladores de eventos. (A lógica de atualização de estado ficará em outro lugar!) Portanto, em vez de “definirtasks” via um manipulador de eventos, você está despachando uma ação de “adicionada/alterada/excluída uma tarefa”. Isso é mais descritivo da intenção do usuário.

O objeto que você passa paradispatché chamado de “ação”:

É um objeto JavaScript regular. Você decide o que colocar nele, mas geralmente deve conter as informações mínimas sobreo que aconteceu. (Você adicionará a própria funçãodispatchem uma etapa posterior.)

Nota

Um objeto de ação pode ter qualquer formato.

Por convenção, é comum dar a ele uma stringtypeque descreve o que aconteceu, e passar qualquer informação adicional em outros campos. Otypeé específico de um componente, então neste exemplo tanto'added'quanto'added_task'seriam aceitáveis. Escolha um nome que diga o que aconteceu!

Etapa 2: Escrever uma função redutora

Uma função redutora é onde você colocará sua lógica de estado. Ela recebe dois argumentos, o estado atual e o objeto de ação, e retorna o próximo estado:

O React definirá o estado como o que você retornar do redutor.

Para mover sua lógica de definição de estado dos seus manipuladores de eventos para uma função redutora neste exemplo, você irá:

  1. Declarar o estado atual (tasks) como o primeiro argumento.
  2. Declarar o objetoactioncomo o segundo argumento.
  3. Retornar o estadopróximodo redutor (que o React definirá como o estado).

Aqui está toda a lógica de definição de estado migrada para uma função redutora:

Como a função redutora recebe o estado (tasks) como um argumento, você podedeclará-la fora do seu componente.Isso diminui o nível de indentação e pode tornar seu código mais fácil de ler.

Observação

O código acima usa instruções if/else, mas é uma convenção usarinstruções switchdentro de redutores. O resultado é o mesmo, mas pode ser mais fácil de ler instruções switch de relance.

Vamos usá-las ao longo do restante desta documentação, assim:

Recomendamos envolver cada blococaseentre chaves{ e }para que variáveis declaradas dentro de diferentescases não entrem em conflito umas com as outras. Além disso, umcasenormalmente deve terminar com umreturn. Se você esquecer de fazerreturn, o código "passará" para o próximocase, o que pode levar a erros!

Se você ainda não se sente confortável com instruções switch, usar if/else é perfeitamente aceitável.

Passo 3: Use o redutor do seu componente

Finalmente, você precisa conectar otasksReducerao seu componente. Importe o HookuseReducerdo React:

Então você pode substituiruseState:

poruseReducerassim:

O HookuseReduceré semelhante aouseState—você deve passar um estado inicial e ele retorna um valor com estado e uma forma de definir o estado (neste caso, a função dispatch). Mas é um pouco diferente.

O HookuseReducerrecebe dois argumentos:

  1. Uma função redutora
  2. Um estado inicial

E retorna:

  1. Um valor com estado
  2. Uma função dispatch (para "despachar" ações do usuário para o redutor)

Agora está totalmente conectado! Aqui, o redutor é declarado na parte inferior do arquivo do componente:

Se quiser, você pode até mover o redutor para um arquivo diferente:

A lógica do componente pode ser mais fácil de ler quando você separa as preocupações assim. Agora os manipuladores de eventos apenas especificamo que aconteceudespachando ações, e a função redutora determinacomo o estado é atualizadoem resposta a elas.

ComparandouseState e useReducer

Redutores não são sem desvantagens! Aqui estão algumas maneiras de compará-los:

  • Tamanho do código:Geralmente, comuseStatevocê tem que escrever menos código inicialmente. ComuseReducer, você tem que escrever tanto uma função redutoraquantodespachar ações. No entanto,useReducerpode ajudar a reduzir o código se muitos manipuladores de eventos modificarem o estado de maneira semelhante.
  • Legibilidade:useStateé muito fácil de ler quando as atualizações de estado são simples. Quando elas ficam mais complexas, podem inflar o código do seu componente e dificultar a leitura. Nesse caso,useReducerpermite que você separe claramente ocomoda lógica de atualização doo que aconteceudos manipuladores de eventos.
  • Depuração:Quando você tem um bug comuseState, pode ser difícil dizerondeo estado foi definido incorretamente epor quê. ComuseReducer, você pode adicionar um log no console no seu redutor para ver cada atualização de estado epor quêaconteceu (devido a qualaction). Se cadaactionestiver correta, você saberá que o erro está na própria lógica do redutor. No entanto, você tem que percorrer mais código do que comuseState.
  • Testes:Um redutor é uma função pura que não depende do seu componente. Isso significa que você pode exportá-lo e testá-lo separadamente de forma isolada. Embora geralmente seja melhor testar componentes em um ambiente mais realista, para lógicas de atualização de estado complexas pode ser útil afirmar que seu redutor retorna um estado específico para um estado inicial e ação específicos.
  • Preferência pessoal:Algumas pessoas gostam de redutores, outras não. Tudo bem. É uma questão de preferência. Você sempre pode converter entreuseState e useReducerde um para o outro: eles são equivalentes!

Recomendamos usar um redutor se você frequentemente encontrar bugs devido a atualizações de estado incorretas em algum componente e quiser introduzir mais estrutura ao seu código. Você não precisa usar redutores para tudo: sinta-se à vontade para misturar e combinar! Você pode até usaruseState e useReducerno mesmo componente.

Escrevendo redutores bem

Mantenha estas duas dicas em mente ao escrever redutores:

  • Os redutores devem ser puros.Semelhante àsfunções de atualização de estado, os redutores são executados durante a renderização! (As ações são enfileiradas até a próxima renderização.) Isso significa que os redutoresdevem ser puros—as mesmas entradas sempre resultam na mesma saída. Eles não devem enviar requisições, agendar timeouts ou realizar quaisquer efeitos colaterais (operações que impactam coisas fora do componente). Eles devem atualizarobjetos e arrayssem mutações.
  • Cada ação descreve uma única interação do usuário, mesmo que isso leve a múltiplas alterações nos dados.Por exemplo, se um usuário pressionar "Redefinir" em um formulário com cinco campos gerenciados por um redutor, faz mais sentido despachar uma açãoreset_formdo que cinco ações separadasset_field. Se você registrar cada ação em um redutor, esse registro deve ser claro o suficiente para que você possa reconstruir quais interações ou respostas aconteceram e em que ordem. Isso ajuda na depuração!

Escrevendo redutores concisos com Immer

Assim como naatualização de objetos e arraysno estado regular, você pode usar a biblioteca Immer para tornar os redutores mais concisos. Aqui, ouseImmerReducerpermite que você mute o estado compushou atribuiçãoarr[i] =:

Os redutores devem ser puros, portanto, eles não devem mutar o estado. Mas o Immer fornece um objeto especialdraftque é seguro para mutar. Nos bastidores, o Immer criará uma cópia do seu estado com as alterações que você fez nodraft. É por isso que os redutores gerenciados poruseImmerReducerpodem mutar seu primeiro argumento e não precisam retornar o estado.

Recapitulação

  • Para converter deuseStateparauseReducer:
    1. Despache ações a partir dos manipuladores de eventos.
    2. Escreva uma função redutora que retorna o próximo estado para um determinado estado e ação.
    3. SubstituauseStateporuseReducer.
  • Os redutores exigem que você escreva um pouco mais de código, mas ajudam na depuração e nos testes.
  • Os redutores devem ser puros.
  • Cada ação descreve uma única interação do usuário.
  • Use o Immer se quiser escrever redutores em um estilo de mutação.

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.