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çãohandleStatusChange
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 umAbortController
- 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:
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)