Do tradutor:
Apresento uma tradução gratuita de um artigo sobre como implementar uma solução eficaz para substituir o Redux pelo contexto e ganchos do React. A indicação de erros na tradução ou no texto é bem-vinda. Aproveite a sua visualização.
Desde o lançamento da nova API de contexto no React 16.3.0, muitas pessoas se perguntaram se a nova API é boa o suficiente para considerá-la uma substituição do Redux? Eu pensei a mesma coisa, mas não entendi completamente, mesmo após o lançamento da versão 16.8.0 com ganchos. Eu tento usar tecnologias populares, o caminho nem sempre é entender toda a gama de problemas que eles resolvem, por isso estou muito acostumado ao Redux.
Por isso, me inscrevi no boletim informativo de
Kent C. Dodds e descobri alguns emails sobre o assunto de gerenciamento de contexto e estado. Eu comecei a ler .... e leia ... e após 5 postagens no blog, algo clicou.
Para entender todos os conceitos básicos por trás disso,
criaremos um botão, clicando no qual receberemos piadas com
icanhazdadjoke e as exibiremos. Este é um exemplo pequeno, mas suficiente.
Para preparar, vamos começar com duas dicas aparentemente aleatórias.
Primeiro, deixe-me apresentar meu amigo
console.count
:
console.count('Button')
Adicionaremos uma chamada
console.count
a cada componente para ver quantas vezes ela está sendo renderizada. Muito legal, né?
Em segundo lugar, quando um componente React é renderizado novamente, ele
não renderiza novamente o conteúdo passado como
children
.
function Parent({ children }) { const [count, setCount] = React.useState(0) console.count('Parent') return ( <div> <button type="button" onClick={() => { setCount(count => count + 1) }}> Force re-render </button> {children} </div> ) } function Child() { console.count('Child') return <div /> } function App() { return ( <Parent> <Child /> </Parent> ) }
Após alguns cliques no botão, você verá o seguinte conteúdo no console:
Parent: 1 Child: 1 Parent: 2 Parent: 3 Parent: 4
Lembre-se de que essa é uma maneira geralmente negligenciada de melhorar o desempenho do seu aplicativo.
Agora que estamos prontos, vamos criar o esqueleto do nosso aplicativo:
import React from 'react' function Button() { console.count('Button') return ( <button type="button"> Fetch dad joke </button> ) } function DadJoke() { console.count('DadJoke') return ( <p>Fetched dad joke</p> ) } function App() { console.count('App') return ( <div> <Button /> <DadJoke /> </div> ) } export default App
Button
deve receber um gerador de ações (aprox. Action Creator. A tradução é retirada da
documentação do
Redux em russo ) que receberá uma anedota.
DadJoke
deve obter o estado e o
App
exibir os dois componentes usando o contexto do provedor.
Agora crie um componente personalizado e chame-o de
DadJokeProvider
, que dentro dele gerenciará o estado e agrupará os componentes filhos no Provedor de Contexto. Lembre-se de que a atualização de seu estado não renderiza novamente o aplicativo inteiro devido à otimização dos filhos acima no React.
Portanto, crie um arquivo e chame-o de
contexts/dad-joke.js
:
import React from 'react' const DadJokeContext = React.createContext() export function DadJokeContextProvider({ children }) { const state = { dadJoke: null } const actions = { fetchDadJoke: () => {}, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
Também exportamos 2 ganchos para obter o valor do contexto.
export function useDadJokeState() { return React.useContext(DadJokeContext).state } export function useDadJokeActions() { return React.useContext(DadJokeContext).actions }
Agora podemos implementar isso:
import React from 'react' import { DadJokeProvider, useDadJokeState, useDadJokeActions, } from './contexts/dad-joke' function Button() { const { fetchDadJoke } = useDadJokeActions() console.count('Button') return ( <button type="button" onClick={fetchDadJoke}> Fetch dad joke </button> ) } function DadJoke() { const { dadJoke } = useDadJokeState() console.count('DadJoke') return ( <p>{dadJoke}</p> ) } function App() { console.count('App') return ( <DadJokeProvider> <Button /> <DadJoke /> </DadJokeProvider> ) } export default App
Aqui! Graças à API que criamos usando os ganchos. Não faremos mais alterações nesse arquivo ao longo da postagem.
Vamos começar a adicionar funcionalidades ao nosso arquivo de contexto, começando com o estado de
DadJokeProvider
. Sim, podemos apenas usar o gancho
useState
, mas vamos gerenciar nosso estado através do
reducer
, simplesmente adicionando a conhecida e amada funcionalidade
Redux
para nós.
function reducer(state, action) { switch (action.type) { case 'SET_DAD_JOKE': return { ...state, dadJoke: action.payload, } default: return new Error(); } }
Agora podemos passar esse redutor para o gancho
useReducer
e obter piadas com a API:
export function DadJokeProvider({ children }) { const [state, dispatch] = React.useReducer(reducer, { dadJoke: null }) async function fetchDadJoke() { const response = await fetch('https://icanhazdadjoke.com', { headers: { accept: 'application/json', }, }) const data = await response.json() dispatch({ type: 'SET_DAD_JOKE', payload: data.joke, }) } const actions = { fetchDadJoke, } return ( <DadJokeContext.Provider value={{ state, actions }}> {children} </DadJokeContext.Provider> ) }
Deveria funcionar! Clique no botão deve receber e exibir piadas!
Vamos verificar o console:
App: 1 Button: 1 DadJoke: 1 Button: 2 DadJoke: 2 Button: 3 DadJoke: 3
Ambos os componentes são renderizados novamente cada vez que o estado é atualizado, mas apenas um deles realmente o usa. Imagine um aplicativo real no qual centenas de componentes usem apenas ações. Seria bom se pudéssemos fornecer todas essas renderizações opcionais?
E aqui entramos no território da relativa igualdade, então um pequeno lembrete:
const obj = {}
Um componente que usa o contexto será renderizado novamente sempre que o valor desse contexto for alterado. Vejamos o significado do nosso provedor de contexto:
<DadJokeContext.Provider value={{ state, actions }}>
Aqui, criamos um novo objeto durante cada renderizador, mas isso é inevitável, porque um novo objeto será criado toda vez que executarmos uma ação (
dispatch
), por isso é simplesmente impossível armazenar em cache (
memoize
) esse valor.
E tudo parece o fim da história, certo?
Se olharmos para a função
fetchDadJoke
, a única coisa que ela usa no escopo externo é
dispatch
, certo? Em geral, vou contar um pequeno segredo sobre as funções criadas em
useReducer
e
useState
. Por uma questão de brevidade, usarei
useState
como um exemplo:
let prevSetCount function Counter() { const [count, setCount] = React.useState() if (typeof prevSetCount !== 'undefined') { console.log(setCount === prevSetCount) } prevSetCount = setCount return ( <button type="button" onClick={() => { setCount(count => count + 1) }}> Increment </button> ) }
Clique no botão várias vezes e veja o console:
true true true
Você notará que
setCount
a mesma função para cada renderização. Isso também se aplica à nossa função de
dispatch
.
Isso significa que nossa função
fetchDadJoke
não depende de nada que muda ao longo do tempo e não depende de outros geradores de ação; portanto, o objeto de ação precisa ser criado apenas uma vez, na primeira renderização:
const actions = React.useMemo(() => ({ fetchDadJoke, }), [])
Agora que temos um objeto em cache com ações, podemos otimizar o valor do contexto? Na verdade, não, porque, por melhor que otimizemos o objeto de valor, ainda precisamos criar um novo a cada vez, devido a alterações de estado. No entanto, e se movermos um objeto de ação de um contexto existente para um novo? Quem disse que podemos ter apenas um contexto?
const DadJokeStateContext = React.createContext() const DadJokeActionsContext = React.createContext()
Podemos combinar os dois contextos em nosso
DadJokeProvider
:
return ( <DadJokeStateContext.Provider value={state}> <DadJokeActionsContext.Provider value={actions}> {children} </DadJokeActionsContext.Provider> </DadJokeStateContext.Provider> )
E ajustar nossos ganchos:
export function useDadJokeState() { return React.useContext(DadJokeStateContext) } export function useDadJokeActions() { return React.useContext(DadJokeActionsContext) }
E nós terminamos! Sério, baixe quantas piadas quiser e veja por si mesmo.
App: 1 Button: 1 DadJoke: 1 DadJoke: 2 DadJoke: 3 DadJoke: 4 DadJoke: 5
Então você implementou sua própria solução otimizada de gerenciamento de estado! Você pode criar provedores diferentes usando esse modelo de dois contextos para criar seu aplicativo, mas isso não é tudo, você também pode renderizar o mesmo componente de provedor várias vezes! O que ?! Sim, tente a renderização
DadJokeProvider
em vários lugares e veja como a implementação de gerenciamento de estado é dimensionada facilmente!
Liberte sua imaginação e revise por que você realmente precisa do
Redux
.
Agradecimentos a Kent C. Dodds pelos artigos sobre o modelo de dois contextos. Eu nunca o vi em lugar nenhum e parece-me que isso muda as regras do jogo.
Leia as seguintes postagens do blog Kent para obter mais informações sobre os conceitos de que falei:
Quando usar useMemo e useCallbackComo otimizar o valor do contextoComo usar o React Context de maneira eficazGerenciando o estado do aplicativo no React.Um truque simples para otimizar as renderizações no React