useState — Parte 2 — Aprofundando em React Hooks (2021)
Bem-vindos à parte 2 da nossa aula sobre nosso grande hook, useState
! Caso perdeu a parte 1, pula aqui e veja nossa explicação dos conceitos básicos e não tão básicos desse g̵a̵n̵c̵h̵o̵ hook querido.
Na aula de hoje, vamos ver como o React trata do ciclo de render quando usamos useState
, que garantias ele te dá quanto a identidade da função de atualização de estado e como podemos otimizar a inicialização de estado com lazy initialization para otimizar funções custosas. Bora lá?
Renderiza Para Mim
Na parte 1, falamos sobre o useState
servir para manter e atualizar estado entre as chamadas dos componentes (lembrando que não são nada mais do que funções) mas também de servir para notificar ao React que queremos que ele “pinte” a tela, conhecido no mundo frontend (FE) de fazer um render.
Chame o render de renderizar, pintar, atualizar ou redesenhar a tela, fica a seu critério. A única coisa importante que você precisa saber é que uma das coisas mais custosas que uma aplicação FE faz é isso — pintar a tela é caro. Se fazer uma soma a + b
demora 1 unidade de tempo T
, para um computador, renderizar coisas gráficas isso pode facilmente ser milhares, milhões ou bilhões de T
s, dependendo de quanto trabalho é necessário para fazer essa atualização.
É por isso que boa parte da responsabilidade de desenvolvedores de FE é minimizar o número de vezes que vamos rerenderizar algo na tela, justamente para não afetarmos o desempenho percebido das nossas aplicações — via de regra, queremos sempre manter no mínimo 60 quadros por segundo, ou fps, se possível.
⚠️ Atenção: por mais que estamos discutindo otimização aqui, lembre-se que otimizar uma aplicação é um dos últimos passos da sua construção — trazer valor de negócio vale muito mais do que salvar alguns ciclos de clock, ok? Ok, voltando à nossa programação.
Tendo isso em mente, o React foi construído para ajudar muito nessa tarefa. Internamente, o React tem alguns algoritmos inteligentes para minimizar o número de vezes que um render precisa ser feito. No caso do useState
, quando atualizamos um valor usando a função de atualização, tipicamente vamos forçar o React a fazer um rerender, para que possamos mostrar o novo valor no componente. Faça o tests com um console.log
para ver isso em ação
Aperta o botão algumas vezes e seu console amigavelmente apontará:
E isso faz sentido, porque como o valor do nosso exemplo, o count
foi atualizado, o React precisa rerenderizar para que a tela mostre o valor correto para o usuário.
A maioria das pessoas sabem que o useState
força um rerender mas o que muitos não sabem é que o React controla isso para que o render só precise ser feito se um valor novo vai aparecer na tela — ou seja, se um valor de estado não mudar, ele não força um outro render desnecessário. Vamos mudar nosso componente um pouco, criando mais um botão:
Clicando no botão +
, vai disparar vários renders, o que faz sentido. Mas no caso do botão Set to 5
, experimente clicar algumas vezes para ver o que acontece:
Repare que o React faz apenas 1 render (na verdade, 2, mas esqueça disso — ou se estiver curioso, pergunte nos comentários que te respondo :) e nunca mais renderiza o componente enquanto o valor de estado não for um novo valor de estado. Bacana, né?
Mas Não Renderiza Tanto Assim
Essa forma “inteligente” que o React faz um render ou não não é tããããão inteligente assim na prática. Por baixo do panos, quando um setState
é chamado, o React faz uma comparação tripla ===
entre o valor que ele tem salvo e o valor “novo” que está recebendo. Se os dois valores forem iguais, ou seja, se currentValue === newValue
, o React não fará um novo render e isso é bom 👏
Dito isso, uma fonte de c̵a̵g̵a̵d̵a̵ problemas que pode aparecer quando trabalhamos com o useState
é quando estamos salvando o valor de objetos inteiros, como no exemplo:
Estamos usando o useState
para guardar uma instância de user
e temos um botão que reseta esse usuário, colocando o username
dele de volta ao valor original (rpedroni
nesse caso). Mesmo sem nunca mudar o valor do username
, experimenta clicar no botão umas 10 vezes e veja o que acontece.
Se o valor é o mesmo sempre, por que que o React está forçando um novo render?
A resposta é porque a “inteligência” dele é limitada. Como a comparação ===
é feita para ver se o suposto novo valor é novo mesmo e como o JavaScript, quando compara dois objetos distintos (lembra que const a = {}; const b = {}; console.log(a === b);
) vai retornar false
já que esses objetos, por mais que sejam iguais de conteúdo, apontam para coisas diferentes. Consequentemente, um novo ciclo de renderização acontece toda vez, independente se o que está na tela é igual.
Indo além no limites do React para impedir c̵a̵g̵a̵d̵a̵s̵ renders desnecessários, se houve um useState
onde nem-estamos-usando-o-fucking-valor, o React vai rerenderizar o componente, desde que a comparação prevValue !== newValue
seja verdade
…e todos os Renderizô!
vão aparecer lá.
Portanto se atente quando estiver trabalhando com objetos mais complexos com o useState
. Já discutimos que não é a melhor ferramenta para isso, sendo um useReducer
da vida melhor. Uma outra forma de fazer o React não rerenderizar o componente em casos assim é usar o React.memo
(serve para props, mas funciona similarmente), que é um HOC que controla o ciclo de renderização onde você pode impedir se o componente rerenderiza ou não, parecido com o shouldComponentUpdate()
dos componentes de classe.
Me Otimize de Jeito
O useState
possui um modo de inicialização interessante para quando o valor inicial do estado é resultado de uma operação custosa, ou seja, de algo que pode pesar no desempenho do front.
Primeiramente, peço desculpas se travei seu computador ou browser com esse código 😬 Segundamente, imagine que getValue
seja algo útil, calculando algum valor necessário para setar o valor inicial do count
, de uma forma custosa que não podemos otimizar.
Você pode imaginar que o primeiro render desse componente vai ser lerdo porque precisamos fazer o cálculo do valor e salvar no count
. Mas clicando no botão alguma vezes para atualizar o count
, por mais que isso não precisa do valor oriundo do getValue
, força um rerender bastante lerdo. E aí, tem alguma qual a razão disso?
É sempre bom lembrar que componentes funcionais nada mais são do que funções que são chamadas em todo render. Usando o getValue
dentro do MyComponent
, toda vez que o componente renderiza, MESMO que não precisamos do valor retornado do getValue
, o getValue
vai ser chamado.
Uma solução para isso é jogar o getValue
para fora do componente e isso funcionaria, mas pense nua função como getValue(someProp)
, que recebe uma prop
do componente, então nesse caso precisaria necessariamente estar dentro do MyComponent
Para esses casos, o useState
fornece uma inicialização por função chamada lazy initialization (inicialização preguiçosa, isso mesmo). Recebendo uma função, o useState
entende que deve chamar a função no primeiro render e nunca mais depois disso
Identidade Única
Uma última coisa antes de acabar a aula e liberar vocês para o recreio — uma coisa importantíssima que o React garante é que a função de atualização devolvida pelo useState
(a setXXX
) tem identidade estável. Isso significa que essa função será sempre a mesma entre um render e outro. Verifica isso aí no código abaixo:
Clica no botão para rerenderizar o component algumas vezes. E, como mágica:
false
para vocês, experimenta remover o <React.StrictMode>
do app e veja se continua. Se quiser saber a razão disso acontecer, manda a pergunta nos comentários abaixo!Você pode perguntar então “Professor Ricardo, por que isso é importante?”. Excelente pergunta! Mas vamos responder isso na aula que vem, quando veremos nosso próximo g̵a̵n̵c̵h̵o̵ hook amigo, o useEffect
. Te vejo lá!
Espero que esse post tenha ajudado!
Ficou com dúvida ou quer mandar uma real? Deixa nos comentários!
Ah, e me acompanhe também no YouTube: https://bit.ly/3q0TIAU ✌!
Aqui é o Professor Ricardo saindo, fique na paz e até a próxima!