Como criar um gancho de reação personalizado para buscar e armazenar dados em cache

Publicados: 2022-03-10
Resumo rápido ↬ Existe uma grande possibilidade de que muitos componentes em seu aplicativo React precisem fazer chamadas para uma API para recuperar dados que serão exibidos para seus usuários. Já é possível fazer isso usando o método de ciclo de vida componentDidMount() , mas com a introdução de Hooks, você pode construir um hook customizado que irá buscar e armazenar em cache os dados para você. Isso é o que este tutorial irá cobrir.

Se você é um novato no React Hooks, você pode começar verificando a documentação oficial para entender. Depois disso, eu recomendo a leitura de “Getting Started With React Hooks API” de Shedrack Akintayo. Para garantir que você está acompanhando, há também um artigo escrito por Adeneye David Abiodun que aborda as melhores práticas com React Hooks que tenho certeza que serão úteis para você.

Ao longo deste artigo, usaremos a API Hacker News Search para criar um gancho personalizado que podemos usar para buscar dados. Embora este tutorial aborde a API Hacker News Search, faremos com que o gancho funcione de forma que ele retorne a resposta de qualquer link de API válido que passarmos para ele.

Melhores práticas de reação

React é uma fantástica biblioteca JavaScript para construir interfaces de usuário ricas. Ele fornece uma ótima abstração de componentes para organizar suas interfaces em um código que funcione bem, e há praticamente qualquer coisa que você possa usá-lo. Leia um artigo relacionado no React →

Buscando dados em um componente React

Antes dos ganchos do React, era convencional buscar dados iniciais no método de ciclo de vida componentDidMount() , e dados baseados em prop ou mudanças de estado no método de ciclo de vida componentDidUpdate() .

Veja como funciona:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

O método de ciclo de vida componentDidMount é invocado assim que o componente é montado e, quando isso é feito, o que fizemos foi fazer uma solicitação para pesquisar “JavaScript” por meio da API Hacker News e atualizar o estado com base na resposta.

O método de ciclo de vida componentDidUpdate , por outro lado, é invocado quando há uma alteração no componente. Comparamos a consulta anterior no estado com a consulta atual para evitar que o método seja invocado toda vez que definimos “dados” no estado. Uma coisa que obtemos com o uso de ganchos é combinar os dois métodos de ciclo de vida de maneira mais limpa - o que significa que não precisaremos ter dois métodos de ciclo de vida para quando o componente for montado e quando for atualizado.

Mais depois do salto! Continue lendo abaixo ↓

Buscando dados com o gancho useEffect

O gancho useEffect é invocado assim que o componente é montado. Se precisarmos que o gancho seja executado novamente com base em algumas alterações de prop ou de estado, precisaremos passá-las para o array de dependência (que é o segundo argumento do gancho useEffect ).

Vamos explorar como buscar dados com ganchos:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

No exemplo acima, passamos query como uma dependência para nosso hook useEffect . Ao fazer isso, estamos dizendo ao useEffect para rastrear as alterações de consulta. Se o valor da query anterior não for igual ao valor atual, o useEffect invocado novamente.

Com isso dito, também estamos definindo vários status no componente conforme necessário, pois isso transmitirá melhor alguma mensagem para a tela com base em alguns estados finitos status . No estado ocioso , podemos informar aos usuários que eles podem usar a caixa de pesquisa para começar. No estado de busca , poderíamos mostrar um spinner . E, no estado buscado , renderizaremos os dados.

É importante definir os dados antes de tentar definir o status como fetched para evitar uma oscilação que ocorre como resultado de os dados estarem vazios enquanto você define o status fetched .

Criando um gancho personalizado

“Um hook customizado é uma função JavaScript cujo nome começa com 'use' e que pode chamar outros Hooks.”

- React Docs

Isso é realmente o que é, e junto com uma função JavaScript, permite que você reutilize parte do código em várias partes do seu aplicativo.

A definição do React Docs o entregou, mas vamos ver como funciona na prática com um gancho personalizado de contador:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

Aqui, temos uma função regular onde pegamos um argumento opcional, definimos o valor para nosso estado, bem como adicionamos os métodos add e subtract que podem ser usados ​​para atualizá-lo.

Em todos os lugares em nosso aplicativo onde precisamos de um contador, podemos chamar useCounter como uma função regular e passar um initialState para sabermos por onde começar a contar. Quando não temos um estado inicial, o padrão é 0.

Veja como funciona na prática:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

O que fizemos aqui foi importar nosso gancho personalizado do arquivo em que o declaramos, para que pudéssemos usá-lo em nosso aplicativo. Definimos seu estado inicial como 100, portanto, sempre que chamamos add() , ele aumenta count em 1 e sempre que chamamos subtract() , diminui count em 1.

Criando useFetch Hook

Agora que aprendemos como criar um gancho personalizado simples, vamos extrair nossa lógica para buscar dados em um gancho personalizado.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

É praticamente a mesma coisa que fizemos acima, com exceção de ser uma função que recebe query e retorna status e data . E esse é um gancho useFetch que poderíamos usar em vários componentes em nosso aplicativo React.

Isso funciona, mas o problema com essa implementação agora é que ela é específica do Hacker News, então podemos chamá-la de useHackerNews . O que pretendemos fazer é criar um hook useFetch que possa ser usado para chamar qualquer URL. Vamos reformulá-lo para receber um URL!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

Agora, nosso hook useFetch é genérico e podemos usá-lo como quisermos em nossos vários componentes.

Aqui está uma maneira de consumi-lo:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

Nesse caso, se o valor de query for truthy , vamos em frente para definir a URL e se não for, não há problema em passar undefined, pois isso seria tratado em nosso gancho. O efeito tentará ser executado uma vez, independentemente.

Memorizando Dados Buscados

A memoização é uma técnica que usaríamos para garantir que não atingiremos o ponto de extremidade do hackernews se tivermos feito algum tipo de solicitação para buscá-lo em alguma fase inicial. Armazenar o resultado de chamadas de busca caras economizará algum tempo de carregamento dos usuários, aumentando assim o desempenho geral.

Nota : Para mais contexto, você pode conferir a explicação da Wikipedia sobre Memoization.

Vamos explorar como podemos fazer isso!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Aqui, estamos mapeando URLs para seus dados. Então, se fizermos uma solicitação para buscar alguns dados existentes, configuramos os dados do nosso cache local, caso contrário, vamos em frente para fazer a solicitação e definir o resultado no cache. Isso garante que não façamos uma chamada de API quando tivermos os dados disponíveis localmente. Também perceberemos que estamos eliminando o efeito se o URL for falsy , para garantir que não continuemos a buscar dados que não existem. Não podemos fazer isso antes do gancho useEffect , pois isso vai contra uma das regras dos ganchos, que é sempre chamar ganchos no nível superior.

Declarar cache em um escopo diferente funciona, mas faz nosso gancho ir contra o princípio de uma função pura. Além disso, também queremos ter certeza de que o React ajude a limpar nossa bagunça quando não quisermos mais usar o componente. Vamos explorar useRef para nos ajudar a conseguir isso.

Memorizando dados com useRef

useRef é como uma caixa que pode conter um valor mutável em sua .current property .”

- React Docs

Com useRef , podemos definir e recuperar valores mutáveis ​​com facilidade e seu valor persiste durante todo o ciclo de vida do componente.

Vamos substituir nossa implementação de cache por alguma mágica de useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Aqui, nosso cache agora está em nosso gancho useFetch com um objeto vazio como valor inicial.

Empacotando

Bem, eu afirmei que definir os dados antes de definir o status buscado era uma boa ideia, mas também podemos ter dois problemas potenciais com isso:

  1. Nosso teste de unidade pode falhar como resultado do array de dados não estar vazio enquanto estamos no estado de busca. O React pode realmente alterar o estado do lote, mas não pode fazer isso se for acionado de forma assíncrona;
  2. Nosso aplicativo renderiza mais do que deveria.

Vamos fazer uma limpeza final em nosso hook useFetch .,Vamos começar trocando nosso useState s para um useReducer . Vamos ver como isso funciona!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

Aqui, adicionamos um estado inicial que é o valor inicial que passamos para cada um de nossos useState s. Em nosso useReducer , verificamos que tipo de ação queremos executar e definimos os valores apropriados para o estado com base nisso.

Isso resolve os dois problemas que discutimos anteriormente, pois agora podemos definir o status e os dados ao mesmo tempo para ajudar a evitar estados impossíveis e re-renderizações desnecessárias.

Só falta mais uma coisa: limpar nosso efeito colateral. Fetch implementa a API Promise, no sentido de que pode ser resolvida ou rejeitada. Se nosso hook tenta fazer uma atualização enquanto o componente foi desmontado porque algum Promise acabou de ser resolvido, React retornaria Can't perform a React state update on an unmounted component.

Vamos ver como podemos corrigir isso com a limpeza useEffect !

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

Aqui, definimos cancelRequest como true depois de defini-lo dentro do efeito. Portanto, antes de tentarmos fazer alterações de estado, primeiro confirmamos se o componente foi desmontado. Se foi desmontado, pulamos a atualização do estado e se não foi desmontado, atualizamos o estado. Isso resolverá o erro de atualização do estado do React e também evitará condições de corrida em nossos componentes.

Conclusão

Exploramos vários conceitos de ganchos para ajudar a buscar e armazenar dados em cache em nossos componentes. Também passamos pela limpeza do nosso hook useEffect que ajuda a evitar um bom número de problemas em nosso aplicativo.

Se você tiver alguma dúvida, sinta-se à vontade para soltá-las na seção de comentários abaixo!

  • Veja o repositório deste artigo →

Referências

  • “Introduzindo Hooks,” React Docs
  • “Introdução à API React Hooks”, Shedrack Akintayo
  • “Melhores Práticas com React Hooks,” Adeneye David Abiodun
  • “Programação funcional: funções puras”, Arne Brasseur