DEV Community

Cover image for Cancelar requisições Fetch em React useEffect
Eduardo Rabelo
Eduardo Rabelo

Posted on

Cancelar requisições Fetch em React useEffect

O useEffect é um poderoso hook para realizar efeitos em suas aplicações React usando a sintaxe de componentes em funções.

Ao retornar uma função dentro do useEffect estamos entrando na faze de limpeza do efeito.

Como mostra a documentação, em componentes de classe, usaríamos os ciclos de vida componentDidMount e componentWillUnmount:

class FriendStatus extends React.Component {
  constructor(props) { ... }

  componentDidMount() { // [ A ]
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() { // [ B ]
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) { ... }

  render() { ... }
}
Enter fullscreen mode Exit fullscreen mode

O exemplo acima pode ser resumido em:

  • [ A ]: Ao montar o componente, criamos uma inscrição/escuta na API ChatAPI.subscribeToFriendStatus e iremos executar a função handleStatusChange para cada mudança
  • [ B ]: Quando o componente for removido, estamos retirando essa inscrição/escuta, para evitar problemas, como vazamento de memória (memory-leaks)

Assim como mostrado na documentação, usando useEffect, teríamos a seguinte sintaxe:

function FriendStatus(props) {
  ...
  useEffect(() => {
    function handleStatusChange(status) { ... }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    return function cleanup() { // [ C ]
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  ...
}
Enter fullscreen mode Exit fullscreen mode

Perceba que estamos retornando uma função em [ C ], ela será executada pelo React ao remover o componente, removendo corretamente (a declração de função function cleanup() {} é opcional, você pode retornar uma função de seta () => {}, para didática do exemplo, estou copiando a documentação do React).

Com esse conceito fresco em mente, vamos falar da Fetch API.

Fetch API

A interface retornada pela Fetch API nos permite utilizar o Abort API, onde podemos passar um controlador para a requisição e, se necessário, realizar o cancelamento da requisição.

Traduzindo isso para código, teríamos a seguinte sintaxe:

const controller = new AbortController();
const signal = controller.signal();

fetch("minha-url", { ...headers, signal }); // [ D ]

// ... um futuro qualquer
// cancela/aborta [ D ] se ainda estiver em execução
controller.abort()
Enter fullscreen mode Exit fullscreen mode

Não vamos discutir os detalhes do significado "requisição em execução", porém, um ponto que vale a pena comentar é: tome cuidado ao cancelar/abortar requisições que não são GET, por exemplo, POST/PUT/DELETE.

Agora que sabemos como transformar nossa requisição Fetch, podemos ter o seguinte fluxo:

  • Dentro de um useEffect, criamos um AbortController
  • Passamos para nosso fetch o signal
  • Retornamos uma função de limpeza no useEffect e executamos o .abort() dentro dela

Teríamos a seguinte sintaxe:

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal();

  fetch("minha-url", { signal });

  return () => {
    controller.abort();
  }
})
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, estamos cancelando nossa requisição toda vez que o efeito for executado.

Que tal um exemplo prático?

Colocando tudo junto

Utilizando a TheCatApi como serviço, iremos usar a API de paginação para navegar suas respostas.

Teremos o seguinte caso:

  • Começar na página 0 com 5 itens
  • Um botão para adicionar 1 a página
  • Um botão para subtrair 1 a página
  • Listar os resultados

O exemplo complete ficaria assim:

function App() {
  let [state, setState] = React.useState({
    status: "idle",
    page: -1,
    cats: [],
    error: ""
  });

  React.useEffect(() => {
    if (state.page < 0) {
      return;
    }

    let didRun = true;

    setState((prevState) => ({ ...prevState, status: "pending", error: "" }));

    let setCats = (cats) => {
      if (didRun) {
        setState((prevState) => ({ ...prevState, status: "done", cats }));
      }
    };
    let setError = (error) => {
      if (didRun) {
        setState((prevState) => ({ ...prevState, status: "error", error }));
      }
    };

    let url = `https://api.thecatapi.com/v1/images/search?limit=5&page=${state.page}&order=Desc`;
    let controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then((res) => res.json())
      .then(setCats)
      .catch(setError);

    return () => {
      didRun = false;
      controller.abort();
    };
  }, [state.page]);

  let updateBy = (value) => (event) => {
    event.preventDefault();
    setState((prevState) => ({ ...prevState, page: prevState.page + value }));
  };

  return (
    <div className="App">
      <div>
        <button onClick={updateBy(-1)}>-1</button>
        <span> - </span>
        <button onClick={updateBy(+1)}>+1</button>
        <p>{state.status}</p>
        <p>{state.error.message}</p>
      </div>
      <div className="Cats">
        {state.cats.map((cat) => {
          return (
            <div key={cat.id}>
              <img width="96" height="96" src={cat.url} />
            </div>
          );
        })}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Visualmente teríamos:

Ao clicar em -1 e +1 rapidamente, podemos ver as requisições canceladas na aba Network do DevTools do seu navegador:

Finalizando

Você pode encontrar o exemplo complete no meu CodeSandbox:

https://codesandbox.io/s/cancel-fetch-using-abort-api-ktvwz

Ao discutirmos qual seria a melhor opção para evitar uma quantidade absurda de requisições desnecessárias pelo clique do usuário, usar AbortController talvez não seja a melhor opção. As práticas atuais ainda são válidas.

Em outros casos onde a duplicação de requests pode acontecer ao montar/desmontar um componente, utilizar o AbortController pode ajudar no desempenho no lado do cliente.


Qualquer pergunta, estou no Twitter: https://twitter.com/oieduardorabelo

Top comments (0)