Componentes e Hooks devem ser puros
Funções puras realizam apenas um cálculo e nada mais. Isso torna seu código mais fácil de entender, depurar e permite que o React otimize automaticamente seus componentes e Hooks corretamente.
- Por que a pureza é importante?
- Componentes e os Hooks devem ser idempotentes
- Se não precisar sincronizar algum state externo com o React, pode também considerar a utilização de um manipulador de eventos (event handler) se apenas precisar ser atualizado em resposta a uma interação do usuário.
- Efeitos colaterais devem ser executados fora da renderização
- Props and state são imutáveis
- Valores de retorno e os argumentos dos Hooks são imutáveis
- Valores são imutáveis depois de serem passados para o JSX
Por que a pureza é importante?
Um dos conceitos fundamentais que define o React é a pureza. Um componente ou hook puro é aquele que é:
- Idempotente – Obtém sempre o mesmo resultado toda vez que é executado com as mesmas entradas - props, state, contexto para entradas de componentes; e argumentos para entradas de hooks.
- Não tem efeitos colaterais na renderização – O código com efeitos colaterais deve ser executado separadamente da renderização. Como por exemplo um manipulador de eventos (event handler) – em que o usuário interage com a UI e a faz atualizar; ou como um Effect – que é executado após a renderização.
- Não altera valores não locais: Componentes e Hooks não devem nunca modificar valores que não são criados localmente na renderização.
Quando a renderização é mantida pura, o React pode entender como priorizar quais atualizações são mais importantes para o usuário ver primeiro. Isso é possível por causa da pureza do render: como os componentes não têm efeitos colaterais no render, o React pode pausar a renderização de componentes que não são tão importantes para atualizar, e só voltar a eles mais tarde quando for necessário.
Concretamente, isto significa que a lógica de renderização pode ser executada várias vezes de uma forma que permite ao React dar ao usuário uma experiência agradável. No entanto, se o seu componente tiver um efeito colateral não rastreado - como modificar o valor de uma variável global durante a renderização - quando o React executar seu código de renderização novamente, seus efeitos colaterais serão acionados de uma forma que não corresponderá ao que você deseja. Isso geralmente leva a bugs inesperados que podem degradar a forma como seus usuários experimentam seu aplicativo. Você pode ver um exemplo disso na página Keeping Components Pure.
Como é que o React executa o seu código??
O React é declarativo: você diz ao React o que renderizar, e o React vai descobrir como melhor exibir isso para o seu usuário. Para fazer isso, o React tem algumas fases onde ele executa seu código. Você não precisa saber sobre todas essas fases para usar bem o React. Mas em um nível alto, você deve saber qual código é executado em um render, e o que é executado fora dele.
Renderização refere-se ao cálculo de como deve ser a próxima versão da sua UI. Após a renderização, Effects são flushed (significando que eles são executados até que não haja mais nenhum) e podem atualizar o cálculo se os Effects tiverem impactos no layout. O React pega esse novo cálculo e o compara com o cálculo usado para criar a versão anterior da sua UI, então compromete apenas as mudanças mínimas necessárias para o DOM (o que o seu usuário realmente vê) para atualizá-lo para a versão mais recente.
Deep Dive
Uma heurística rápida para saber se o código é executado durante a renderização é examinar onde ele está: se estiver escrito no nível superior, como no exemplo abaixo, há uma boa chance de ser executado durante a renderização.
function Dropdown() {
const selectedItems = new Set(); // criado durante a renderização
// ...
}
Manipuladores de eventos e efeitos não são executados na renderização:
function Dropdown() {
const selectedItems = new Set();
const onSelect = (item) => {
// este código está em um manipulador de eventos, logo, só é executado quando o usuário o aciona
selectedItems.add(item);
}
}
function Dropdown() {
const selectedItems = new Set();
useEffect(() => {
// este código está dentro de um Effect, por isso só é executado após a renderização
logForAnalytics(selectedItems);
}, [selectedItems]);
}
Componentes e os Hooks devem ser idempotentes
Os componentes devem devolver sempre o mesmo resultado relativamente às suas entradas - props, state e contexto. Isto é conhecido como idempotência. Idempotência é um termo popularizado na programação funcional. Refere-se à ideia de que obtém sempre o mesmo resultado de cada vez que executa esse pedaço de código com as mesmas entradas.
Isto significa que todo o código que corre durante a renderização também tem de ser idempotente para que esta regra se mantenha. Por exemplo, esta linha de código não é idempotente (e, portanto, o componente também não é):
function Clock() {
const time = new Date(); // 🔴 Mau: apresenta sempre um resultado diferente!!
return <span>{time.toLocaleString()}</span>
}
new Date()
não é idempotente, uma vez que devolve sempre a data atual e altera o seu resultado sempre que é chamado. Ao renderizar o componente acima, a hora exibida na tela ficará presa na hora em que o componente foi renderizado. Da mesma forma, funções como Math.random()
também não são idempotentes, pois retornam resultados diferentes a cada vez que são chamadas, mesmo quando as entradas são as mesmas.
Isso não significa que você não deva usar funções não-idempotentes como new Date()
em tudo - você deve apenas evitar usá-las durante a renderização. Neste caso, podemos sincronizar a última data para este componente usando um Effect:
import { useState, useEffect } from 'react'; function useTime() { // 1. Mantém o controle do estado da data atual. `useState` recebe uma função inicializadora como seu // estado inicial. Ela é executada apenas uma vez quando o hook é chamado, então apenas a data atual // no momento em que o hook é chamado é definida inicialmente. const [time, setTime] = useState(() => new Date()); useEffect(() => { // 2. Atualiza a data atual a cada segundo usando `setInterval`. const id = setInterval(() => { setTime(new Date()); // ✅ Bom: o código não-idempotente não é mais executado na renderização }, 1000); // 3. Retorna uma função de limpeza para não vazar o timer `setInterval`. return () => clearInterval(id); }, []); return time; } export default function Clock() { const time = useTime(); return <span>{time.toLocaleString()}</span>; }
Ao encapsular a chamada não-idempotente new Date()
em um Effect, ele move esse cálculo para fora da renderização.
Se não precisar sincronizar algum state externo com o React, pode também considerar a utilização de um manipulador de eventos (event handler) se apenas precisar ser atualizado em resposta a uma interação do usuário.
Efeitos colaterais devem ser executados fora da renderização
Efeitos colaterais não devem ser executados no render, pois o React pode renderizar componentes várias vezes para criar a melhor experiência possível para o usuário.
Enquanto o render deve ser mantido puro, efeitos colaterais são necessários em algum momento para que sua aplicação faça algo interessante, como mostrar algo na tela! O ponto chave dessa regra é que efeitos colaterais não devem rodar no render, já que o React pode renderizar componentes múltiplas vezes. Na maioria dos casos, você vai usar event handlers para lidar com efeitos colaterais. Usar um manipulador de eventos diz explicitamente ao React que esse código não precisa ser executado durante a renderização, mantendo a renderização pura. Se você já esgotou todas as opções - e apenas como último recurso - você também pode lidar com efeitos colaterais usando useEffect
.
Quando é que se pode ter uma mutação?
Mutação local
Um exemplo comum de efeito colateral é a mutação, que em JavaScript se refere à alteração do valor de uma variável não-primitiva. Em geral, embora a mutação não seja idiomática no React, a mutação local é absolutamente boa:
function FriendList({ friends }) {
const items = []; // ✅ Bom: criado localmente
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ Bom: a mutação local é aceitável
}
return <section>{items}</section>;
}
Não há necessidade de contorcer o seu código para evitar a mutação local. Array.map
também poderia ser usado aqui por questões de brevidade, mas não há nada de errado em criar um array local e então colocar itens nele durante a renderização.
Mesmo que pareça que estamos mutando items
, o ponto chave a ser observado é que este código só faz isso localmente - a mutação não é “lembrada” quando o componente é renderizado novamente. Em outras palavras, items
só permanece enquanto o componente estiver presente. Como items
é sempre recriado toda vez que <FriendList />
é renderizado, o componente sempre retornará o mesmo resultado.
Por outro lado, se items
foi criado fora do componente, ele mantém seus valores anteriores e se lembra das mudanças:
const items = []; // 🔴 Mau: criado fora do componente
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 Mau: altera um valor criado fora da renderização
}
return <section>{items}</section>;
}
Quando <FriendList />
for executado novamente, continuaremos anexando friends
a items
toda vez que o componente for executado, levando a múltiplos resultados duplicados. Esta versão de <FriendList />
tem efeitos colaterais observáveis durante a renderização e quebra a regra.
Inicialização preguiçosa
A inicialização preguiçosa também é boa, apesar de não ser totalmente “pura”:
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Bom: se não afetar outros componentes
// Continua a renderização
}
Alterar o DOM
Os efeitos colaterais que são diretamente visíveis para o utilizador não são permitidos na lógica de renderização dos componentes React. Por outras palavras, a simples chamada de uma função de componente não deve, por si só, produzir uma alteração na tela.
function ProductDetailPage({ product }) {
document.window.title = product.title; // 🔴 Mau: Altera o DOM
}
Uma maneira de alcançar o resultado desejado de atualizar window.title
fora da renderização é sincronizar o componente com window
.
Desde que chamar um componente várias vezes seja seguro e não afete a renderização de outros componentes, o React não se importa se é 100% puro no sentido estrito de programação funcional da palavra. É mais importante que componentes devem ser idempotentes.
Props and state são imutáveis
As props e o state de um componente são imutáveis snapshots. Nunca altere-os diretamente. Ao invés disso, passe novas props para baixo, e use a função setter de useState
.
Você pode pensar nos valores de props e state como snapshots que são atualizados após a renderização. Por esse motivo, você não modifica as variáveis props ou state diretamente: em vez disso, você passa novas props, ou usa a função setter fornecida para dizer ao React que o state precisa ser atualizado na próxima vez que o componente for renderizado.
Não alterar Props
As props são imutáveis porque, se as alterar, a aplicação produzirá resultados inconsistentes, o que pode ser difícil de depurar, uma vez que pode ou não funcionar, dependendo das circunstâncias.
function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 Mau: nunca alterar diretamente as props
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ Bom: fazer uma cópia
return <Link url={url}>{item.title}</Link>;
}
Não alterar state
O useState
devolve a variável de estado e um setter para atualizar esse state.
const [stateVariable, setter] = useState(0);
Em vez de atualizar a variável de estado no local, precisamos atualizá-la usando a função setter que é retornada por useState
. Alterar valores na variável state não faz com que o componente seja atualizado, deixando seus usuários com uma UI desatualizada. Usar a função setter informa ao React que o state foi alterado e que precisamos enfileirar uma nova renderização para atualizar a interface do usuário.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🔴 Mau: nunca alterar diretamente o state
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ Bom: utilizar a função setter devolvida por useState
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
Valores de retorno e os argumentos dos Hooks são imutáveis
Uma vez que os valores são passados para um hook, não deve modificá-los. Como os props em JSX, os valores tornam-se imutáveis quando passados para um hook.
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 Mau: nunca alterar diretamente os argumentos do hook
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Bom: fazer uma cópia
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
Um princípio importante no React é o raciocínio local: a capacidade de entender o que um componente ou hook faz apenas analisando seu código isoladamente. Hooks devem ser tratados como ‘caixas pretas’ quando são chamados. Por exemplo, um hook personalizado pode ter usado seus argumentos como dependências para memorizar valores internamente:
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}
Se você alterar os argumentos dos Hooks, a memorização do hook personalizado ficará incorreta, portanto, é importante evitar fazer isso.
style = useIconStyle(icon); // `style` é memorizado com base em `icon`
icon.enabled = false; // Mau: 🔴 nunca alterar diretamente os argumentos do gancho
style = useIconStyle(icon); // o resultado previamente memorizado é devolvido
style = useIconStyle(icon); // `style` é memorizado com base em `icon`
icon = { ...icon, enabled: false }; // Bom: ✅ fazer uma cópia
style = useIconStyle(icon); // o novo valor de `style` é calculado
Da mesma forma, é importante não modificar os valores de retorno dos Hooks, pois eles podem ter sido memorizados.
Valores são imutáveis depois de serem passados para o JSX
Não altere os valores depois de eles terem sido usados no JSX. Mova a mutação antes que o JSX seja criado.
Quando você usa JSX em uma expressão, o React pode avaliar avidamente o JSX antes que o componente termine de renderizar. Isso significa que mutar valores depois que eles foram passados para o JSX pode levar a UIs desatualizadas, já que o React não saberá atualizar a saída do componente.
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 Mau: os estilos já foram utilizados no JSX acima
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ Bom: criamos um novo valor
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}