Cancelar requisições Fetch em React useEffect

Image for post
Image for post

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

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);
};
});
...
}

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

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();
}
})

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

Visualmente teríamos:

Image for post
Image for post

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

Image for post
Image for post

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

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store