
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.