React Query: um guia prático

React Query: um guia prático

O artigo a seguir tem como objetivo dar uma visão geral do que é o React Query, suas vantagens e algumas dicas valiosas para o seu dia a dia.

O React é um framework para construção de interfaces que não vem com um conjunto de ferramentas fixas de desenvolvimento, o que obriga o desenvolvedor a complementá-lo com outras ferramentas que agilizam o trabalho.

Durante o desenvolvimento, um dos maiores desafios é trazer os dados do servidor e disponibilizá-los para toda a nossa aplicação. Isto implica:

  • Gerenciar o status da solicitação.
  • Colocar todos os dados no estado React.
  • Atualizar constantemente com os dados do servidor.
  • Fazer abstrações para evitar a repetição da lógica.

Uma das bibliotecas que resolve grande parte desses problemas é o React Apollo, que não só faz requisições ao servidor, como também inclui um cache para salvar esses dados e consultá-los posteriormente.

O mais surpreendente dessa biblioteca é que além de incluir os dados do servidor, ela também torna esses dados acessíveis para que toda a aplicação possa utilizá-los, mas tudo isso é possível se você trabalhar com Graphql. E se você trabalhar com REST? É aqui que entra o React Query.

Introdução ao React Query

O React Query tira muito do que há de bom no React Apollo, com a diferença de que você define a camada onde faz as solicitações ao servidor. O React Query apenas pega uma promise e armazena em cache o resultado dessa promessa, além de incluir o tratamento de estado de erro e promise.

Se você ainda não consegue ver a vantagem de todos os itens acima, deixe-me dar um exemplo onde trabalharemos com uma lista de contatos, que vem de um servidor. Vamos cobri-lo com e sem React Query.

Exemplo sem o React Query

import baseApi from "../lib/baseApi";
import { useEffect, useState } from "react";
import { IContact } from "../types";

export const WithoutReactQuery = () => {
  const [contacts, setContacts] = useState<IContact[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<{ message: string } | null>(null);
  useEffect(() => {
    setLoading(true);
    baseApi
      .get("/contacts")
      .then((result) => result.data)
      .then((data) => {
        setContacts(data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {loading ? (
        <h3>loading...</h3>
      ) : (
        <ul>
          {contacts.map((contact) => (
            <li key={contact.id}>
              {contact.name} | {contact.lastName}
            </li>
          ))}
        </ul>
      )}
    </div>
  );

Bem, este é um cenário típico em que fazemos uma solicitação à nossa API e obtemos os contatos. Todo o processo de buscar os dados e colocá-los no estado React, lidar com o erro e exibir um status de carregamento nos levou cerca de 18 linhas ou mais. A longo prazo, um código como esse é difícil de manter e se torna muito repetitivo.

Ejemplo com o React Query

Para usar o React Query, precisamos iniciá-lo. Siga os passos da imagem a seguir:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/*rest of code*/}
    </QueryClientProvider>
  );
}

Esta é a configuração básica que o React Query precisa para funcionar. Entraremos em detalhes mais tarde sobre a configuração que podemos aplicar.

Ok, agora vamos voltar ao exemplo de um momento atrás.

export const WithReactQuery = () => {
  const queryInfo = useQuery(["contacts"], () => {
    return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
  });

  if (queryInfo.isError) {
    return <div>Error: {(queryInfo.error as any).message}</div>;
  }

  return (
    <div>
      {queryInfo.isLoading ? (
        <h3>loading...</h3>
      ) : (
        <ul>
          {queryInfo.data.map((contact) => (
            <li key={contact.id}>
              {contact.name} | {contact.lastName}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

Muito menos código do que acima, certo :)?

Talvez você esteja um pouco perdido/a com o que acabamos de ver. Não te preocupes! Passo a passo.

Queries e Mutations

O React Query propõe duas formas de realizar operações, que são Queries e Mutations.

Se você já tocou no Graphql antes, pode estar familiarizado com esse conceito, mas vamos revisá-lo de qualquer maneira.

Queries:

Qualquer operação que envolva apenas a leitura de dados do servidor, em um método REST, estaríamos falando de GET.

No exemplo anterior fizemos uma consulta para obter os contatos.

const queryInfo = useQuery(["contacts"], () => {
  return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
});

O primeiro parâmetro passado para o gancho é uma matriz que atua como um conjunto de chaves para identificar esse resultado armazenado em cache. Isso pode ser dinâmico. Por exemplo, se adicionarmos um filtro, nosso código ficaria assim:

export const WithReactQuery = () => {
  const [name, setName] = useState("");
  const queryInfo = useQuery(["contacts", name], async () => {
    const params = new URLSearchParams();
    params.set("name", name);
    return baseApi
      .get<IContact[]>("/contacts", {
        params: params,
      })
      .then((d) => d.data);
  });
// rest of code

Isso significa que para cada requisição com um nome diferente, teremos uma chave diferente e, portanto, dados diferentes.

Mutations

Qualquer operação que implique uma modificação nos dados do servidor, em REST seria POST, DELETE, PUT.

Ao contrário de uma consulta, o resultado de uma mutação não é armazenado em cache.

Agora vamos ver como salvar um contato. Para executar uma mutação de React Query, exporte useMutation.

export const SaveContactComponent = () => {
  const saveContactMutation = useMutation({
    mutationFn: async (input: IContactInput) => {
      return baseApi.post("/contact", input);
    },
  });
  return (
    <div
      className={`
    w-screen h-screen flex justify-center items-center bg-blue-600
    `}
    >
      {saveContactMutation.isLoading && <div>Saving..</div>}
      <form
        className="flex flex-col gap-3"
        onSubmit={(e) => {
          e.preventDefault();
          saveContactMutation.mutate(values);
        }}
      >
        {/* inputs */}
        <button
          className="bg-violet-400 rounded-md p-2 text-white"
          type="submit"
        >
          Submit
        </button>
      </form>
    </div>
  );
};

Como dissemos antes, uma mutação é uma alteração no servidor. Por exemplo, aqui adicionamos um contato, o que implica que a lista de contatos será alterada. Agora, como atualizamos a lista de contatos?

Invalidar Queries

Cada requisição está relacionada a uma chave específica e isso significa que quando uma requisição é feita, o React Query irá primeiro perguntar se esta chave existe no cache. Se existir, ele irá recuperá-lo rapidamente, mas também podemos dizer ao React Query que esses dados não são mais válidos e trazê-los de volta do servidor.

Dito isso, o hook useMutation ficaria assim:

// rest of code
const saveContactMutation = useMutation({
  mutationFn: async (input: IContactInput) => {
    const result = baseApi.post("/contact", input);
    await queryClient.invalidateQueries({
      queryKey: ["contacts"],
    });
    return result;
  },
});

Com o exposto, atualizaremos todas as consultas que tenham contatos como chave.
Isso também atualiza as consultas com o filtro de name. Se este não for o comportamento desejado, você pode adicionar o flag exact.

Se você quiser um pouco mais de informação sobre isso, vá para a documentação.

Pontos chave

Agora que vimos um pouco sobre o funcionamento básico do React Query, vamos ver alguns pontos que podem nos ajudar a utilizar melhor esta biblioteca.

Utilizar custom hooks

Para evitar chamadas repetidas para o servidor, geralmente geramos essas solicitações para que sejam acessíveis a partir de componentes filhos. O React Query muda um pouco nossa forma de pensar, pois não precisamos nos preocupar se nossas chamadas serão feitas várias vezes, já que tudo depende da chave.

Vejamos um exemplo:

export const Parent = ({ children }) => {
  const queryInfo = useQuery(["contacts"], async () => {
    return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
  });

  return <div>{children}</div>;
};

export const Child = () => {
  const queryInfo = useQuery(["contacts"], async () => {
    return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
  });

  return <div></div>;
};

export const App = () => {
  return (
    <div>
      <Parent>
        <Child />
      </Parent>
    </div>
  );
};

No código acima, o componente Child possui uma consulta com a chave de contatos e o componente Parent também possui uma consulta, então acontecerá o seguinte:

  • O componente Parent assina os dados pela primeira vez. Como o React Query não armazena esses dados em cache, ele faz a solicitação ao servidor.
  • O componente Child assina os dados uma segunda vez. Se os dados existirem em cache, não teremos mais uma segunda chamada ao servidor.

Dito isto, podemos usar Custom Hooks para facilitar o acesso a esses dados.

export const useContacts = () => {
  return useQuery(["contacts"], async () => {
    return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
  });
};

export const Parent = ({ children }) => {
  const queryInfo = useContacts();

  return <div>{children}</div>;
};

export const Child = () => {
  const queryInfo = useContacts();
  return <div></div>;
};

export const App = () => {
  return (
    <div>
      <Parent>
        <Child />
      </Parent>
    </div>
  );
};

Assim conseguimos reaproveitar o código e, se tivermos que fazer alterações nessa requisição, fazemos em um só lugar.

Diferença entre o StaleTime e o CacheTime

O React Query baseia sua lógica de cache em stale-while-revalidate. Este tópico é um pouco amplo, mas basicamente consiste em atualizar os dados por trás (fazer a solicitação ao servidor) e validá-los no cache atual. Se isso mudar, ele atualiza a interface do usuário, caso contrário, não faz nada. Por tudo isso, oferecemos uma melhor experiência do usuário.

Mas do que se trata?

React Query nos dá a possibilidade de mudar o comportamento do cache. Vejamos alguns conceitos:

StaleTime: Refere-se à duração entre quando os dados são considerados novos até se tornarem obsoletos. Se os dados armazenados em cache estiverem atualizados, não é feita uma validação no servidor para verificar se estão corretos ou não.

Por padrão, esse valor é 0. No exemplo que abordamos, toda vez que uma consulta é chamada novamente, ela quase sempre faz uma chamada para o servidor. Lembro que quando estudei isso, pensei que o acima estraga o significado do React Query porque não estaria nos salvando chamadas, mas me enganei: a chamada que eles fazem para o servidor é para revalidar. Se o React Query verificar que nosso cache está correto, essa solicitação passará despercebida pelo usuário.

CacheTime: É o tempo decorrido enquanto as consultas inativas são totalmente removidas do cache. Por padrão, são 5 minutos.

Até o momento não tive a necessidade de modificar essa propriedade porque, por exemplo, se você não tem nenhum componente vigiando a lista de contatos por 5 minutos, significa que ela não é exibida em nenhum lugar. Então, por que armazenar isso em cache?

Pensando nisso, podemos fazer essa configuração diretamente no hook...

export const useContacts = () => {
 return useQuery(
    ["contacts"],
    async () => {
      return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
    },
    {
      staleTime: 1000,
    }
  );
};
};

...ou diretamente na instância do cliente, considerando que esta seria uma configuração geral.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000,
    },
  },
});

Queries dependentes

Com certeza em algum momento seu query terá uma dependência de algo externo. Digo isso porque todas as consultas são feitas assim que o componente é montado na tela, mas podemos mudar isso.

Vamos supor que agora você queira mostrar a lista de contatos, mas com um filtro específico e este não está presente ao montar o componente. É aqui que a propriedade enabled entra em ação.

const App = () => {
  const queryInfo = useQuery(
    ["contacts", name],
    async () => {
      const params = new URLSearchParams();
      params.set("name", name);
      return baseApi
        .get<IContact[]>("/contacts", {
          params: params,
        })
        .then((d) => d.data);
    },
    {
      enabled: !!name && name.length > 0,
    }
  );

  return <div></div>;
};

Adicionando enabled: !!name && name.length > 0 dizemos a esta consulta que ela não deve ser executada até que o nome exista e seu tamanho seja maior que zero.

Select Property

Se você já usou o Redux antes, o React Query certamente o fascinará. Aqui perguntas apropriadas: E quanto aos seletores?

Nem sempre temos os filtros no servidor: em algum momento temos que filtrar isso no cliente.

Dentro da opção de consulta encontramos Select, que nos permite fazer algo com o resultado da função que passamos para queryFn e também subscrever apenas a alteração daquela parte específica dos dados.

Se quiséssemos fazer um filtro no cliente por nome teríamos o seguinte:

const App = () => {
  const [name, setName] = useState("");
  const queryInfo = useQuery(
    ["contacts"],
    async () => {
      const params = new URLSearchParams();
      params.set("name", name);
      return baseApi.get<IContact[]>("/contacts").then((d) => d.data);
    },
    {
      select: (data) => data.filter((d) => d.name.indexOf(name) !== -1),
    }
  );

  return (
    <div>
      <input
        value={name}
        onChange={(e) => {
          setName(e.target.value);
        }}
      />
    </div>
  );
};

Dessa forma, filtramos o resultado e não necessariamente fizemos uma chamada para o servidor. Recomenda-se usar useCallback para memorizar a função.

Client State Vs Server State

Em algum momento, uma pergunta como "não preciso mais do Redux?" pode surgir. Talvez algo semelhante.

Client State

Para lidar com o estado do cliente, você pode usar recursos ou bibliotecas do React como Jotai ou Zuztand.

Se você planeja usar o Redux junto com o React Query, não recomendo. Na verdade, o Redux tem o RTk, um pacote complementar do Redux baseado no React Query.

Espero que este artigo o motive a usar esta biblioteca e comece a usá-la em seus projetos. Tentei cobrir todos os conceitos básicos e alguns que são muito úteis na prática. Depois de ver o grande potencial desta biblioteca, você pode continuar com a documentação.

Deixo algumas palavras-chave que podem te ajudar a continuar buscando informações.

  • Infinite Queries.
  • Optimistic Updates.
  • SSR with React Query.


Happy code!

⚠️
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.