Frontend & Mobile Nathan Geeksman

React Hooks: Erros comuns e como evitar re-renderizações desnecessárias.

React Hooks: Erros comuns e como evitar re-renderizações desnecessárias.

React Hooks: Erros comuns e como evitar re-renderizações desnecessárias.

Introdução

O desenvolvimento de aplicações Web utilizando a biblioteca React é uma das escolhas mais populares no atual cenário de tecnologia. Uma das principais razões para essa popularidade está relacionada à facilidade na criação de interfaces de usuário interativas e escaláveis, graças ao conceito dos componentes reutilizáveis.

Nesse contexto, os Hooks foram introduzidos como uma alternativa aos HOCs (Higher-Order Components) e Context API, oferecendo um novo modo de usar o estado local e as funções do ciclo de vida dentro das próprias componentes funcionais. Isso proporcionou grande flexibilidade para a criação de aplicações mais complexas.

No entanto, ao trabalhar com os Hooks em React, é comum enfrentar problemas relacionados à otimização do desempenho da aplicação. Re-renderizações desnecessárias podem ocorrer quando as dependências dos Hooks não são cuidadosamente gerenciadas, o que pode resultar no aumento significativo da carga computacional e até mesmo na perda de performance.

Este artigo visa apresentar alguns dos erros mais comuns encontrados ao usar os Hooks em React e fornecer estratégias para evitar re-renderizações desnecessárias. Ao final desta leitura, você estará equipado com as ferramentas necessárias para desenvolver aplicações que sejam tanto produtivas quanto eficientes em termos de desempenho.

O que é e por que importa

Os Hooks são uma extensão das funções de estado e ciclo de vida para as componentes funcionais em React, permitindo que você utilize o estado local e funções do ciclo de vida dentro delas sem precisar renderizá-las como componentes. Isso significa que você pode lidar com dependências, efeitos e outras operações complexas diretamente no escopo das suas funções.

A motivação por trás dos Hooks é resolver alguns problemas relacionados à gerência de estado e ciclo de vida em React, os quais são historicamente gerenciados por meio de HOCs ou Context API. Os Hooks fornecem uma abordagem mais simples e direta para lidar com essas necessidades complexas, permitindo que as aplicações sejam mais fáceis de entender e mais fáceis de manter.

Uma das principais razões pelas quais os Hooks são importantes está relacionada à otimização do desempenho. Ao permitir uma maior gestão sobre quando a aplicação deve re-renderizar, os Hooks ajudam a evitar que a aplicação seja forçada a atualizar desnecessariamente, o que pode ser um problema significativo em aplicações de grande escala ou aquelas com requisitos altos de desempenho.

Como funciona na prática

Os Hooks funcionam por meio de uma série de etapas, que podem ser resumidas como segue:

  • Inclusão: É necessária a inclusão dos hooks específicos no topo da componente funcional. Essa inclusão é feita pela função useHook, onde o nome do hook é passado para ela. Por exemplo, const [estado, atualizar] = useState(inicial);
  • Evalução das dependências: Os Hooks analisam as dependências e determinam se a execução da lógica dentro deles deve ser executada novamente. Isso ocorre porque os Hooks são pensados para lidar com estado mutável e também podem ter funções de ciclo de vida que precisam ser chamadas em momentos específicos.
  • Execução da lógica: Se as dependências mudaram, a lógica dentro dos Hooks é executada. Isso pode incluir atualizações do estado ou efeitos colaterais associados à execução do código dentro dos Hooks.
  • Atualização do componente: Se o estado foi alterado de alguma forma, a função render da aplicação é chamada novamente para refletir as mudanças no modelo de dados.

Exemplo real

Um exemplo concreto de como evitar re-renderizações desnecessárias é o caso de uso de um componente que mostra uma lista de itens, onde apenas a parte do componente deve ser atualizada quando algum item mudar.

import React, { useState, useEffect } from 'react';

function ListaDeItens() {
  // Inclusão dos hooks necessários.
  const [lista, setLista] = useState([
    { id: 1, nome: "Item 1" },
    { id: 2, nome: "Item 2" }
  ]);

  useEffect(() => {
    // Verifica se a lista foi atualizada.
    if (lista !== []) {
      const elementosDaLista = document.querySelectorAll(".item");
      elementosDaLista.forEach((elemento, indice) => {
        elemento.textContent = lista[indice].nome;
      });
    }
  }, [lista]);

  return (
    <div>
      {lista.map((item, index) => (
        <div key={item.id} className="item"></div>
      ))}
    </div>
  );
}

// Utilizando a função useState para manter uma lista de itens.
function App() {
  const [nomeDoItem, setNomeDoItem] = useState("");

  // Função que atualiza o nome do item na lista.
  function atualizarNomeDoItem(nome) {
    const novoItem = { id: lista.length + 1, nome: nome };
    setLista([...lista, novoItem]);
    setNomeDoItem("");
  }

  return (
    <div>
      <input
        type="text"
        value={nomeDoItem}
        onChange={(e) => setNomeDoItem(e.target.value)}
      />
      <button onClick={() => atualizarNomeDoItem(nomeDoItem)}>Adicionar Item</button>
      <ListaDeItens lista={lista} setLista={setLista} />
    </div>
  );
}

export default App;

Esse exemplo utiliza o useState para manter uma lista de itens e o useEffect é usado para verificar se a lista foi atualizada. Quando uma nova item é adicionado, apenas a parte do componente que mostra a lista é atualizada.

O código comentado acima representa um caso de uso real onde o componentes ListaDeItens e App são usados para exibir uma lista de itens. O useState é usado para manter a lista de itens, e o useEffect é usado para verificar se a lista foi atualizada.

Boas práticas

Utilize o useCallback para evitar a re-criação de funções

Evite a re-criação desnecessária de funções dentro do componente ao usar useEffect. Em vez disso, utilize useCallback para armazenar as funções em um memoizado objeto.

const meuComponente = () => {
  const minhaFuncao = useCallback(() => {
    // código aqui...
  }, [lista]);

  useEffect(() => {
    // código aqui...
  }, [lista]);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Utilize o useMemo para evitar a re-criação de valores

Evite a re-criação desnecessária de valores dentro do componente ao usar useEffect. Em vez disso, utilize useMemo para armazenar os valores em um memoizado objeto.

const meuComponente = () => {
  const meuValor = useMemo(() => {
    // código aqui...
  }, [lista]);

  useEffect(() => {
    // código aqui...
  }, [lista]);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Utilize o useCallback e useMemo para evitar a re-criação de funções e valores

Combine os dois exemplos acima para evitar a re-criação desnecessária de funções e valores dentro do componente.

const meuComponente = () => {
  const minhaFuncao = useCallback(() => {
    // código aqui...
  }, [lista]);

  const meuValor = useMemo(() => {
    // código aqui...
  }, [lista]);

  useEffect(() => {
    // código aqui...
  }, [lista]);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Utilize o useImperativeHandle para passar props a components filhos

Se um componente pai precisa passar props a componentes filho, utilize useImperativeHandle para criar uma interface de acesso a essas props.

const meuComponente = () => {
  const meuRef = React.createRef();

  useEffect(() => {
    // código aqui...
  }, []);

  useImperativeHandle(meuRef, () => ({
    minhaFuncao: () => {
      // código aqui...
    },
  }));

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Utilize o useContext para passar dados entre componentes

Se um componente pai precisa passar dados a componentes filho, utilize useContext para criar uma interface de acesso a esses dados.

const contexto = React.createContext();

const App = () => {
  const [meuValor, setMeuValor] = useState('');

  return (
    <contexto.Provider value={meuValor}>
      {/* código aqui... */}
    </contexto.Provider>
  );
};

const MeuCriativoComponente = () => {
  const meuValor = useContext(contexto);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Armadilhas comuns

Não utilize useEffect para atualizar o estado do componente

Evite utilizar useEffect para atualizar o estado do componente, pois isso pode causar uma cascata de re-renderizações desnecessárias.

const meuComponente = () => {
  const [meuValor, setMeuValor] = useState('');

  useEffect(() => {
    setMeuValor('novo valor');
  }, []);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Não utilize useState para armazenar valores computados

Evite utilizar useState para armazenar valores computados, pois isso pode causar uma re-criação desnecessária do componente.

const meuComponente = () => {
  const meuValor = useState(() => {
    // código aqui...
  })[0];

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Não utilize useCallback e useMemo para armazenar valores computados

Evite utilizar useCallback e useMemo para armazenar valores computados, pois isso pode causar uma re-criação desnecessária do componente.

const meuComponente = () => {
  const meuValor = useCallback(() => {
    // código aqui...
  }, []);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Não utilize useContext para passar props a componentes filhos

Evite utilizar useContext para passar props a componentes filhos, pois isso pode causar uma cascata de re-renderizações desnecessárias.

const contexto = React.createContext();

const App = () => {
  const [meuValor, setMeuValor] = useState('');

  return (
    <contexto.Provider value={meuValor}>
      {/* código aqui... */}
    </contexto.Provider>
  );
};

const MeuCriativoComponente = () => {
  const meuValor = useContext(contexto);

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Não utilize useImperativeHandle para passar props a componentes filhos

Evite utilizar useImperativeHandle para passar props a componentes filhos, pois isso pode causar uma cascata de re-renderizações desnecessárias.

const meuComponente = () => {
  const meuRef = React.createRef();

  useEffect(() => {
    // código aqui...
  }, []);

  useImperativeHandle(meuRef, () => ({
    minhaFuncao: () => {
      // código aqui...
    },
  }));

  return (
    <div>
      {/* código aqui... */}
    </div>
  );
};

Conclusão

Para evitar re-renderizações desnecessárias no React, é importante entender os conceitos de Hooks e como utilizá-los corretamente. Alguns dos erros mais comuns incluem o uso indevido de useCallback e useMemo para armazenar valores computados, a passagem de props utilizando useContext, e a utilização de useImperativeHandle para passar props a componentes filhos.

Para melhorar a performance do seu aplicativo React, é recomendado:

  • Utilizar useCallback e useMemo corretamente para armazenar valores computados apenas quando necessário;
  • Passar props utilizando o padrão de fluxo de dados em vez de utilizar useContext;
  • Evitar a utilização de useImperativeHandle para passar props a componentes filhos.

Além disso, é fundamental testar e otimizar seu aplicativo regularmente para garantir que as melhorias sejam implementadas corretamente. Para isso, utilize ferramentas como o React DevTools para identificar e solucionar problemas relacionados à performance do seu aplicativo.

Referências

  • React. useState. Disponível em: https://pt-br.reactjs.org/docs/hooks-state.html Acesso: 2024.
  • MDN Web Docs. useContext. Disponível em: https://developer.mozilla.org/pt-BR/docs/Web/react/hooks#Usecontext Acesso: 2024.
  • Martin Fowler. CQRS. Disponível em: https://martinfowler.com/bliki/CQRS.html Acesso: 2024.
  • ThoughtWorks. CQRS and Event Sourcing in .NET Core. Disponível em: https://www.thoughtworks.com/insights/blog/cqrs-and-event-sourcing-net-core Acesso: 2024.
  • Microsoft Docs. Hook de contexto (React). Disponível em: https://docs.microsoft.com/pt-br/dotnet/react/guides/hook-contexto Acesso: 2024.