
Você já ouviu falar sobre "elevar o estado"? Eu acho que você tem e essa é a razão exata pela qual você está aqui. Como é possível que um dos 12 principais conceitos listados na documentação oficial do React leve a um desempenho ruim? Neste artigo, consideraremos uma situação em que realmente é o caso.
Etapa 1: levante
Eu sugiro que você crie um jogo simples de jogo da velha. Para o jogo, precisaremos de:
- Algum estado do jogo. Nenhuma lógica de jogo real para descobrir se ganhamos ou perdemos. Apenas uma matriz bidimensional simples preenchida com - undefined,- "x"ou- "0".
 
 -  - const size = 10 
 
 
- Um contêiner pai para hospedar o estado do nosso jogo. 
 -  - const App = () => { const [field, setField] = useState(initialField) return ( <div> {field.map((row, rowI) => ( <div> {row.map((cell, cellI) => ( <Cell content={cell} setContent={ // Update a single cell of a two-dimensional array // and return a new two dimensional array (newContent) => setField([ // Copy rows before our target row ...field.slice(0, rowI), [ // Copy cells before our target cell ...field[rowI].slice(0, cellI), newContent, // Copy cells after our target cell ...field[rowI].slice(cellI + 1), ], // Copy rows after our target row ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div> ) }
 
 
- Um componente filho para exibir o estado de uma única célula. 
 -  - const randomContent = () => (Math.random() > 0.5 ? 'x' : '0') const Cell = ({ content, setContent }) => ( <div onClick={() => setContent(randomContent())}>{content}</div> )
 
 
Demonstração ao vivo # 1
Até agora, parece bem. Um campo perfeitamente reativo com o qual você pode interagir na velocidade da luz :) Vamos aumentar o tamanho. Digamos, para 100. Sim, é hora de clicar no link de demonstração e alterar a variável de size no topo. Ainda rápido para você? Experimente o 200 ou use a otimização da CPU incorporada no Chrome . Você vê agora um atraso significativo entre o momento em que você clica em uma célula e o momento em que seu conteúdo muda?
Vamos mudar o size volta para 10 e adicionar alguns perfis para investigar a causa.
 const Cell = ({ content, setContent }) => { console.log('cell rendered') return <div onClick={() => setContent(randomContent())}>{content}</div> } 
Demonstração ao vivo # 2
Sim, é isso. console.log simples seria suficiente, pois é executado em todas as renderizações.
Então o que vemos? Com base no número das instruções "cell renderizadas" (para size = N, deve ser N) em nosso console, parece que todo o campo é renderizado novamente cada vez que uma única célula é alterada.
A coisa mais óbvia a fazer é adicionar algumas chaves, como sugere a documentação do React .
 <div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} setContent={(newContent) => setField([ ...field.slice(0, rowI), [ ...field[rowI].slice(0, cellI), newContent, ...field[rowI].slice(cellI + 1), ], ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div> 
Demonstração ao vivo nº 3
No entanto, depois de aumentar o size novamente, vemos que esse problema ainda está lá. Se pudéssemos ver por que algum componente é renderizado ... Felizmente, podemos com alguma ajuda do incrível React DevTools . É capaz de registrar por que os componentes são renderizados. Você precisa ativá-lo manualmente.

Uma vez ativado, podemos ver que todas as células foram renderizadas novamente porque seus objetos foram alterados, especificamente, o setContent .

Cada célula possui dois objetos: content e setContent . Se a célula [0] [0] for alterada, o conteúdo da célula [0] [1] não será alterado. Por outro lado, setContent captura field , cellI e rowI em seu fechamento. cellI e rowI permanecem os mesmos, mas o field muda a cada alteração de qualquer célula.
Vamos refatorar nosso código e manter setContent o mesmo.
Para manter a referência para setContent a mesma, devemos nos livrar dos fechamentos. Podemos eliminar o fechamento rowI e rowI , fazendo com que nosso Cell passe explicitamente cellI e rowI para setContent . Quanto ao field , poderíamos utilizar um recurso interessante de setState - ele aceita retornos de chamada .
 const [field, setField] = useState(initialField)  
O que faz com que o App fique assim
 <div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} rowI={rowI} cellI={cellI} setContent={setCell} /> ))} </div> ))} </div> 
Agora Cell precisa passar cellI e rowI para o setContent .
 const Cell = ({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) } 
Demonstração ao vivo # 4
Vamos dar uma olhada no relatório do DevTools.

O que ?! Por que diabos diz "adereços parentais alterados"? Então, o problema é que toda vez que nosso campo é atualizado, o App é renderizado novamente. Portanto, seus componentes filhos são renderizados novamente. Ok O stackoverflow diz algo útil sobre a otimização do desempenho do React? A Internet sugere usar o shouldComponentUpdate ou seus parentes próximos: PureComponent e memo .
 const Cell = memo(({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) }) 
Demonstração ao vivo # 5
Yay Agora, apenas uma célula é renderizada novamente quando o conteúdo é alterado. Mas espere ... Houve alguma surpresa? Seguimos as melhores práticas e obtivemos o resultado esperado.
Uma risada maligna deveria estar aqui. Como não estou com você, por favor, tente o máximo possível para imaginá-lo. Vá em frente e aumente o size na demonstração ao vivo nº 5 . Desta vez, você pode ter que ir com um número um pouco maior. No entanto, o atraso ainda está lá. Por que ???
Vamos dar uma olhada no relatório DebTools novamente.

Há apenas uma renderização do Cell e foi bem rápida, mas também uma renderização do App , que levou algum tempo. O fato é que, a cada nova renderização do App cada Cell precisa comparar seus novos objetos com os objetos anteriores. Mesmo que decida não renderizar (que é precisamente o nosso caso), essa comparação ainda leva tempo. O (1), mas esse O (1) ocorre size * size vezes!
Etapa 2: mova-o para baixo
O que podemos fazer para contornar isso? Se a renderização do App nos custar muito, teremos que parar de renderizar o App . Não é possível se continuar hospedando nosso estado no App usando useState , porque é exatamente isso que desencadeia as renderizações novamente. Portanto, temos que mudar nosso estado para baixo e deixar que cada Cell inscreva no estado por conta própria.
Vamos criar uma classe dedicada que será um contêiner para o nosso estado.
 class Field { constructor(fieldSize) { this.size = fieldSize  
Então, nosso App pode ficar assim:
 const App = () => { return ( <div> {// As you can see we still need to iterate over our state to get indexes. field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} /> ))} </div> ))} </div> ) } 
E nosso Cell pode exibir o conteúdo do field por conta própria:
 const Cell = ({ rowI, cellI }) => { console.log('cell render') const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 
Demonstração ao vivo # 6
Nesse ponto, podemos ver nosso campo sendo renderizado. No entanto, se clicarmos em uma célula, nada acontece. Nos logs, podemos ver "setCell" para cada clique, mas a célula permanece em branco. A razão aqui é que nada diz à célula para renderizar novamente. Nosso estado fora do React muda, mas o React não sabe disso. Isso tem que mudar.
Como podemos disparar uma renderização programaticamente?
Com as classes, temos forceUpdate . Isso significa que precisamos reescrever nosso código em classes? Na verdade não. O que podemos fazer com os componentes funcionais é introduzir algum estado fictício, que mudamos apenas para forçar nosso componente a renderizar novamente.
Veja como podemos criar um gancho personalizado para forçar as renderizações novamente.
 const useForceRender = () => { const [, setDummy] = useState(0) const forceRender = useCallback(() => setDummy((oldVal) => oldVal + 1), []) return forceRender } 
Para acionar uma nova renderização quando nosso campo é atualizado, precisamos saber quando ele é atualizado. Isso significa que precisamos poder, de alguma forma, assinar atualizações de campo.
 class Field { constructor(fieldSize) { this.size = fieldSize this.data = new Array(this.size).fill(new Array(this.size).fill(undefined)) this.subscribers = {} } _cellSubscriberId(rowI, cellI) { return `row${rowI}cell${cellI}` } cellContent(rowI, cellI) { return this.data[rowI][cellI] } setCell(rowI, cellI, newContent) { console.log('setCell') this.data = [ ...this.data.slice(0, rowI), [ ...this.data[rowI].slice(0, cellI), newContent, ...this.data[rowI].slice(cellI + 1), ], ...this.data.slice(rowI + 1), ] const cellSubscriber = this.subscribers[this._cellSubscriberId(rowI, cellI)] if (cellSubscriber) { cellSubscriber() } } map(cb) { return this.data.map(cb) }  
Agora podemos assinar atualizações de campo.
 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => field.subscribeCellUpdates(rowI, cellI, forceRender), [ forceRender, ]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 
Demonstração ao vivo # 7
Vamos jogar com size com esta implementação. Tente aumentá-lo para os valores que antes pareciam atrasados. E ... é hora de abrir uma boa garrafa de champanhe! Temos um aplicativo que processa uma célula e uma célula apenas quando o estado dessa célula muda!
Vamos dar uma olhada no relatório do DevTools.

Como podemos ver agora, apenas o Cell está sendo renderizado e é uma loucura rápida.
E se dizer que agora o código da nossa Cell é uma causa potencial de vazamento de memória? Como você pode ver, em useEffect , useEffect atualizações de células, mas nunca cancelamos a inscrição. Isso significa que, mesmo quando o Cell é destruído, sua assinatura permanece. Vamos mudar isso.
Primeiro, precisamos ensinar a Field o que significa cancelar a inscrição.
 class Field {  
Agora podemos aplicar unsubscribeCellUpdates ao nosso Cell .
 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 
Demonstração ao vivo # 8
Então, qual é a lição aqui? Quando faz sentido mover o estado para baixo na árvore de componentes? Nunca! Bem, na verdade não. :) Siga as práticas recomendadas até que elas falhem e não faça otimizações prematuras. Honestamente, o caso que consideramos acima é um pouco específico, no entanto, espero que você o lembre se precisar exibir uma lista realmente grande.
Etapa bônus: refatoração do mundo real
Na demonstração ao vivo nº 8 , usamos o field global, o que não deve ser o caso em um aplicativo do mundo real. Para resolvê-lo, poderíamos hospedar o field em nosso App e passá-lo para a árvore usando [context] ().
 const AppContext = createContext() const App = () => {  
Agora podemos consumir field partir do contexto em nossa Cell .
 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() const field = useContext(AppContext) useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 
Demonstração ao vivo # 9
Felizmente, você encontrou algo útil para o seu projeto. Sinta-se livre para me comunicar seus comentários! Certamente aprecio qualquer crítica e pergunta.