Mantendo Componentes Puros

Algumas funções JavaScript são puras. Funções puras apenas executam um cálculo e nada mais. Escrevendo seus componentes estritamente como funções puras, você pode evitar toda uma classe de erros desconcertantes e comportamento imprevisível à medida que seu código base cresce. No entanto, para obter esses benefícios, existem algumas regras que você deve seguir.

Você aprenderá

  • O que é pureza e como ela ajuda você a evitar erros
  • Como manter os componentes puros, mantendo as alterações fora da fase de renderização
  • Como usar o Modo Estrito para encontrar erros em seus componentes

Pureza: Componentes como fórmulas

Em ciência da computação (e especialmente no mundo da programação funcional), uma função pura é uma função com as seguintes características:

  • Ela cuida de seus próprios negócios. Ela não altera nenhum objeto ou variável que existia antes de ela ser chamada.
  • Mesmas entradas, mesma saída. Dadas as mesmas entradas, uma função pura deve sempre retornar o mesmo resultado.

Você pode já estar familiarizado com um exemplo de funções puras: fórmulas em matemática.

Considere esta fórmula matemática: y = 2x.

Se x = 2 então y = 4. Sempre.

Se x = 3 então y = 6. Sempre.

Se x = 3, y às vezes não será 9 ou –1 ou 2.5 dependendo da hora do dia ou do estado do mercado de ações.

Se y = 2x e x = 3, y sempre será 6.

Se fizéssemos isso em uma função JavaScript, ficaria assim:

function double(number) {
return 2 * number;
}

No exemplo acima, double é uma função pura. Se você passar 3, ela retornará 6. Sempre.

React é projetado em torno desse conceito. React assume que todo componente que você escreve é uma função pura. Isso significa que os componentes React que você escreve devem sempre retornar o mesmo JSX dadas as mesmas entradas:

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Ferva {drinkers} xícaras de água.</li>
      <li>Adicione {drinkers} colheres de chá de chá e {0.5 * drinkers} colheres de chá de especiaria.</li>
      <li>Adicione {0.5 * drinkers} xícaras de leite para ferver e açúcar a gosto.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Receita de Chai com Especiarias</h1>
      <h2>Para dois</h2>
      <Recipe drinkers={2} />
      <h2>Para uma reunião</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Quando você passa drinkers={2} para Recipe, ele retornará JSX contendo 2 xícaras de água. Sempre.

Se você passar drinkers={4}, ele retornará JSX contendo 4 xícaras de água. Sempre.

Assim como uma fórmula matemática.

Você pode pensar em seus componentes como receitas: se você as seguir e não introduzir novos ingredientes durante o processo de cozimento, você obterá o mesmo prato toda vez. Esse “prato” é o JSX que o componente serve ao React para renderizar.

Uma receita de chá para x pessoas: pegue x xícaras de água, adicione x colheres de chá de chá e 0.5x colheres de chá de especiarias, e 0.5x xícaras de leite

Illustrated by Rachel Lee Nabors

Efeitos colaterais: consequências (não)intencionais

O processo de renderização do React deve ser sempre puro. Os componentes devem apenas retornar seu JSX e não alterar nenhum objeto ou variável que existia antes da renderização — isso os tornaria impuros!

Aqui está um componente que quebra essa regra:

let guest = 0;

function Cup() {
  // Ruim: mudando uma variável pré-existente!
  guest = guest + 1;
  return <h2>Xícara de chá para o convidado #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Este componente está lendo e escrevendo uma variável guest declarada fora dele. Isso significa que chamar este componente várias vezes produzirá diferentes JSX! E o que é mais, se outros componentes lerem guest, eles também produzirão diferentes JSX dependendo de quando foram renderizados! Isso não é previsível.

Voltando à nossa fórmula y = 2x, agora mesmo se x = 2, não podemos confiar que y = 4. Nossos testes podem falhar, nossos usuários ficariam perplexos, os aviões cairiam do céu — você pode ver como isso levaria a erros confusos!

Você pode corrigir este componente passando guest como uma prop em vez disso:

function Cup({ guest }) {
  return <h2>Xícara de chá para o convidado #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Agora seu componente é puro, pois o JSX que ele retorna só depende da prop guest.

Em geral, você não deve esperar que seus componentes sejam renderizados em nenhuma ordem específica. Não importa se você chama y = 2x antes ou depois de y = 5x: ambas as fórmulas serão resolvidas independentemente uma da outra. Da mesma forma, cada componente deve apenas “pensar por si mesmo” e não tentar coordenar ou depender de outros durante a renderização. A renderização é como um exame escolar: cada componente deve calcular JSX sozinho!

Deep Dive

Detectando cálculos impuros com StrictMode

Embora você possa não ter usado todos eles ainda, no React existem três tipos de entradas que você pode ler ao renderizar: props, state e context. Você sempre deve tratar essas entradas como somente leitura.

Quando você deseja alterar algo em resposta à entrada do usuário, você deve definir o state em vez de escrever em uma variável. Você nunca deve alterar variáveis ou objetos pré-existentes enquanto seu componente está renderizando.

React oferece um “Modo Estrito” no qual ele chama a função de cada componente duas vezes durante o desenvolvimento. Ao chamar as funções do componente duas vezes, o Modo Estrito ajuda a encontrar componentes que quebram essas regras.

Observe como o exemplo original exibiu “Convidado #2”, “Convidado #4” e “Convidado #6” em vez de “Convidado #1”, “Convidado #2” e “Convidado #3”. A função original era impura, então chamá-la duas vezes a quebrou. Mas a versão pura corrigida funciona mesmo se a função for chamada duas vezes toda vez. Funções puras apenas calculam, então chamá-las duas vezes não mudará nada — assim como chamar double(2) duas vezes não altera o que é retornado, e resolver y = 2x duas vezes não altera o que é y. Mesmas entradas, mesmas saídas. Sempre.

O Modo Estrito não tem efeito em produção, por isso não diminuirá a velocidade do aplicativo para seus usuários. Para participar do Modo Estrito, você pode envolver seu componente raiz em <React.StrictMode>. Algumas estruturas fazem isso por padrão.

Mutação local: O pequeno segredo do seu componente

No exemplo acima, o problema era que o componente alterava uma variável pré-existente durante a renderização. Isso geralmente é chamado de “mutação” para fazê-lo soar um pouco mais assustador. Funções puras não mutam variáveis fora do escopo da função ou objetos que foram criados antes da chamada — isso as torna impuras!

No entanto, é perfeitamente aceitável alterar variáveis e objetos que você acabou de criar durante a renderização. Neste exemplo, você cria uma array [], atribui a ela a variável cups e, em seguida, push uma dúzia de xícaras nela:

function Cup({ guest }) {
  return <h2>Xícara de chá para o convidado #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

Se a variável cups ou a array [] fossem criadas fora da função TeaGathering, isso seria um grande problema! Você estaria alterando um objeto pré-existente inserindo itens nessa array.

No entanto, está tudo bem porque você os criou durante a mesma renderização, dentro de TeaGathering. Nenhum código fora de TeaGathering saberá que isso aconteceu. Isso é chamado de “mutação local” — é como o pequeno segredo do seu componente.

Onde você pode causar efeitos colaterais

Embora a programação funcional dependa fortemente da pureza, em algum momento, em algum lugar, algo tem que mudar. Esse é o ponto da programação! Essas mudanças — atualizar a tela, iniciar uma animação, alterar os dados — são chamadas de efeitos colaterais. São coisas que acontecem “à parte”, não durante a renderização.

No React, efeitos colaterais geralmente pertencem a manipuladores de eventos. Os manipuladores de eventos são funções que o React executa quando você executa alguma ação — por exemplo, ao clicar em um botão. Embora os manipuladores de eventos sejam definidos dentro do seu componente, eles não são executados durante a renderização! Portanto, os manipuladores de eventos não precisam ser puros.

Se você esgotou todas as outras opções e não consegue encontrar o manipulador de eventos certo para seu efeito colateral, você ainda pode anexá-lo ao seu JSX retornado com uma chamada useEffect em seu componente. Isso diz ao React para executá-lo mais tarde, após a renderização, quando os efeitos colaterais são permitidos. No entanto, essa abordagem deve ser seu último recurso.

Quando possível, tente expressar sua lógica apenas com a renderização. Você ficará surpreso com o quão longe isso pode te levar!

Deep Dive

Por que o React se importa com a pureza?

Escrever funções puras requer um certo hábito e disciplina. Mas também abre oportunidades maravilhosas:

  • Seus componentes podem ser executados em um ambiente diferente — por exemplo, no servidor! Como eles retornam o mesmo resultado para as mesmas entradas, um componente pode atender a muitas solicitações de usuários.
  • Você pode melhorar o desempenho ignorando a renderização de componentes cujas entradas não foram alteradas. Isso é seguro porque as funções puras sempre retornam os mesmos resultados, por isso são seguras para cache.
  • Se alguns dados forem alterados no meio da renderização de uma árvore de componentes profunda, o React pode reiniciar a renderização sem perder tempo para terminar a renderização desatualizada. A pureza torna seguro parar de calcular a qualquer momento.

Cada novo recurso do React que estamos construindo tira proveito da pureza. Da obtenção de dados a animações e desempenho, manter os componentes puros desbloqueia o poder do paradigma React.

Recap

  • Um componente deve ser puro, o que significa:
    • Ele cuida de seus próprios negócios. Ele não deve alterar nenhum objeto ou variável que existia antes da renderização.
    • Mesmas entradas, mesma saída. Dadas as mesmas entradas, um componente deve sempre retornar o mesmo JSX.
  • A renderização pode acontecer a qualquer momento, portanto, os componentes não devem depender da sequência de renderização uns dos outros.
  • Você não deve mutar nenhuma das entradas que seus componentes usam para renderização. Isso inclui props, state e context. Para atualizar a tela, “defina” o state em vez de mutar objetos pré-existentes.
  • Esforce-se para expressar a lógica do seu componente no JSX que você retorna. Quando você precisar “mudar as coisas”, geralmente vai querer fazê-lo em um manipulador de eventos. Como último recurso, você pode usar useEffect.
  • Escrever funções puras leva um pouco de prática, mas desbloqueia o poder do paradigma do React.

Challenge 1 of 3:
Consertar um relógio quebrado

Este componente tenta definir a classe CSS do <h1> para "night" durante o horário da meia-noite às seis da manhã e "day" em todos os outros horários. No entanto, não funciona. Você pode consertar este componente?

Você pode verificar se sua solução funciona alterando temporariamente o fuso horário do computador. Quando a hora atual estiver entre meia-noite e seis da manhã, o relógio deve ter cores invertidas!

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}