Cancelar requisições Fetch em React useEffect
Tags: ptbr, webdev, javascript, react
Published: 2020-10-15 04:01:56

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](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup).

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

```jsx
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() { ... }
}
```

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:

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

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

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

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](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) nos permite utilizar o [Abort API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), 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:

```js
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()
```

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:

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

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

  return () => {
    controller.abort();
  }
})
```

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](https://docs.thecatapi.com/pagination) 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:

```jsx
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>
  );
}
```

Visualmente teríamos:

![](https://dev-to-uploads.s3.amazonaws.com/i/gllkdjrxoq2yctumhhce.png)

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

![](https://dev-to-uploads.s3.amazonaws.com/i/5zzsf2okdw26d9jw7pib.png)

## 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