Alternativa interna do Redux com contexto de reação e ganchos

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') // Button: 1 console.count('Button') // Button: 2 console.count('App') // App: 1 console.count('Button') // Button: 3 

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 = {} //       console.log(obj === obj) // true //        //  2   console.log({} === {}) // false 

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 useCallback
Como otimizar o valor do contexto
Como usar o React Context de maneira eficaz
Gerenciando o estado do aplicativo no React.
Um truque simples para otimizar as renderizações no React

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


All Articles