React Hook SWR - Melhor UX no consumo de API no Front End React

✅ Pré-requisitos

No decorrer do post, vamos mostrar alguns exemplos práticos. Para melhor experiência na leitura do post, você precisa entender:

🔰 Introdução

Vercel criou o React Hook SWR para entregar uma melhor experiência ao usuário (UX), o segredo está em como a ferramenta lida com os dados que foram buscados independente da implementação do back end (Java, Node.js  PHP) e da API que a realiza a busca no Front End, por exemplo fetch API do JavaScript e Axios amplamente utilizados para essa finalidade.

A ferramenta em questão não substitui o fetch API ou Axios, ela é um camada que executa em cima dessas tecnologias — como um Wrapper (algo que envolve). Abaixo veremos com mais detalhes.

SWR é uma sigla para Stale-While-Revalidate (tradução literal: traga os dados antigos enquanto avalia se tem novos dados). Essa é uma estratégia que pega os dados do cache e depois, por baixo dos panos, é feito uma requisição ao back end para trazer os novos dados se possuir, recarregando a listagem com os novos dados, de maneira que fique transparente para o usuário.

Com o SWR, os componentes vão receber o fluxo de atualizações de dados constante e automaticamente. A interface do usuário será sempre rápida e reativa.

O que o SWR entrega:

  • Um rápido, leve e reusável data fetching;
  • Cache integrado e evita redundância de requisição;
  • Experiência Real-time;
  • Suporte ao TypeScript;
  • Possibilidade de ser utilizado no React Native;
  • Navegação rápida entre páginas;
  • Revalidação dos dados quando a tela recebe foco (revalidate-on-focus);
  • Revalidação de dados quando reconecta à Internet;
  • Mutação de dados locais com (Optimistic UI);
  • React Suspense;
  • API agnóstica, pode ser utilizado com REST ou GraphQL;
  • Back end agnóstico, não importa em qual linguagem foi implementado.

🤝 Conhecendo a API do SWR:

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
  • data = os dados retornados da API;
  • error =  caso algum erro tenha ocorrido;
  • isValidating = Boolean que retorna true se estiver buscando os dados da API e false quando já retornou os dados, ou seja, já foi feita a validação;
  • mutate = função que irá alterar algum valor no cache.

👊 SWR na Prática

Para começar a usar o SWR é necessário instalar a lib na aplicação React:

yarn add swr

Depois é necessário criar um hook que recebe uma string com a URL e retorna os dados e um erro:

import useSWR from "swr";

export function useFetch(url: string) {
  const { data, error } = useSWR(url, async (url) => {
    const response = await fetch(url);
    const data = await response.json();

    return data;
  });

  return { data, error };
}

useFetch é um hook customizado que estamos criando. Dentro dela tem a chamada para um um outro hook customizado useSWR que importamos da biblioteca swr.

O hook recebe uma string e uma função assíncrona, perceba que ela é um wrapper pois envolve a chamada à API logo abaixo, que nesse caso estamos usando fetch API que recebe URL do servidor e retorna os dados assim que a promise for resolvida, e por fim retorna o dado e o erro que são retornadas do hook useFetch.

Só isso e já podemos ver todo o poder do SWR no front end.

Para demonstrar o suporte ao TypeScript e a possibilidade de usar axios, podemos criar o mesmo código de  cima, dessa maneira:

import useSWR from 'swr'
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:3333'
})

export function useFetch<Data = any, Error = any>(url: string) {
  const { data, error } = useSWR<Data, Error>(url, async url => {
    const response = await api.get(url);

    return response.data;
  })

  return { data, error }
}

Usando o useSWR temos como vantagem deixar o componente mais enxuto, evitando utilizar o useEffect para requisição, observe as diferenças abaixo:

  • Sem o SWR:
const UserList: React.FC = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch("users").then((response) => {
      response.json().then((users) => {
        setData(users);
      });
    });
  }, []);

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>
          <Link to={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
};

export default UserList;
  • Agora com SWR:
const UserList: React.FC = () => {
  const { data } = useFetch("users");

  if (!data) {
    return <p>Carregando...</p>;
  }

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>
          <Link to={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
};

export default UserList;

Observe que ficou mais simples.

Enquanto os dados não chegam, fica exibindo uma mensagem de Carregando.... na tela. Essa é uma das vantagens de usar o SWR, código fica mais limpo.

O SWR sempre busca os dados no cache e depois é feito a revalidação, buscando os dados do servidor.

🌀 Mutação de Dados

Utilizando a estratégia de Optimist UI (Interface otimista), é exibido os dados na tela do usuário como se já estivesse dado certo alguma alteração, inclusão ou deleção. Por baixo dos panos, o back end faz o seu trabalho, porém na interface já exibe como se o dado já estivesse pronto, isto é, feito toda operação no back end.

Essa é uma técnica bem interessante para dar uma sensação de velocidade para o usuário, e tem um caso de uso especifico, sempre que um ação "dá certo" no back end pode ter essa técnica implementada no front end.

👩‍💻 Exemplo prático da utilização do mutate

Precisamos que o hook useSWR retorne o mutate e que o mesmo seja retornado no hook useFetch:

import useSWR from 'swr'
import api from '../services/api';

export function useFetch<Data = any, Error = any>(url: string) {
  const { data, error, mutate } = useSWR<Data, Error>(url, async url => {
    const response = await api.get(url);

    return response.data;
  })

  return { data, error, mutate }
}

Agora podemos utilizar essa função nos componentes:

  • Código:
import React, { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { mutate as mutateGlobal } from 'swr';
import { useFetch } from '../hooks/useFetch';
import api from '../services/api';

interface User {
  id: number;
  name: string;
}

const UserList: React.FC = () => {
  const { data, mutate } = useFetch<User[]>('users');

  const handleNameChange = useCallback((id: number) => {
    api.put(`users/${id}`, { name: 'Bartolomeu' });

    const updatedUsers = data?.map(user => {
      if (user.id === id) {
        return { ...user, name: 'Bartolomeu' }
      }

      return user;
    })

    mutate(updatedUsers, false)
    mutateGlobal(`users/${id}`, { id, name: 'Bartolomeu' })
  }, [data, mutate]);

  if (!data) {
    return <p>Carregando...</p>
  }

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          <Link to={`/users/${user.id}`}>
            {user.name}
          </Link>
          <button type="button" onClick={() => handleNameChange(user.id)}>
            Alterar nome
          </button>
        </li>
      ))}
    </ul>
  );
}

export default UserList;

Tem um botão que quando clicado chama a função handleNameChange a qual chama a api na rota users/${id} informando o name como parâmetro da requisição.

Todas as chamada à uma API são assíncronas, mas nesse caso não estamos usando await para esperar que requisição seja concluída, com isso o código continua a sua execução, sem aguardar o término da instrução.

É criado um novo array com os dados alterados e repassado para a função mutate que irá atualizar o cache e renderizar essa alteração, independente do que for acontecer no back end.

A flag false, no segundo parâmetro da função mutate, indica que não deve fazer a busca dos dados, uma vez que eles ainda podem não ter sido salvos no banco de dados. Vale lembrar que não estamos utilizando o await na alteração.

A função renomeada para mutateGlobal atualiza os dados em outras rotas, o mutate que usamos altera apenas no componente local.

Uma vez que alteramos de maneira otimista a listagem de usuários (UserList), precisamos alterar também o detalhe do usuário (UserDetail), caso contrário a listagem fica atualizada, mas os detalhes do usuário não, gerando inconsistência.

Então, observamos que alguns cuidados devem ser tomados na implementação dessa técnica.

Com SWR a requisição não fica mais rápida, mas a maneira de como é gerenciado o cache da aplicação melhora tanto a experiência do usuário que deixa essa impressão.

🧑‍💻 Experimentando o SWR

Se quiser experimentar a aplicação, inicie o servidor abrindo essa página:

API back end com json-server

E depois abra a aplicação front end:

React - Front End com SWR

Pode fazer o fork desse código para sua conta no CodeSandbox e testar à vontade.

👍 Conclusão

SWR melhora a experiência do usuário que utiliza a aplicação, criando a sensação do app estar mais rápido. Para o dev, no entanto, ele tem à sua disposição funções que simplificam a implementação de estratégias de revalidação de dados, cache, melhorias na busca, entre outros benefícios que podemos encontrar na documentação oficial. Ou seja, SWR amplia a experiência em dois caminhos: desenvolvedores e usuários.

SWR não está sozinho no mercado, tem o React Query que segue a mesma proposta.

Aqui na Rocketseat estamos utilizando o SWR. Melhora bastante a usabilidade da aplicação.

O Diego Fernandes gravou um vídeo explicando direto ao ponto as principais funcionalidades do SWR. Vale a pena dar uma conferida:

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀