Tipar useEffect no React com TypeScript
O useEffect parece simples, mas esconde armadilhas de tipagem que pegam até devs experientes. Aprenda a tipar cleanup functions, lidar com async e usar AbortController do jeito certo.
Por que isso é importante
Tipar useEffect no React com TypeScript. O useEffect parece simples, mas esconde armadilhas de tipagem que pegam até devs experientes. Aprenda a tipar cleanup functions, lidar com async e usar AbortController do jeito certo.
O que o TypeScript realmente checa no useEffect
Diferente do useState ou useRef, o useEffect nao recebe um tipo generico. O TypeScript valida o useEffect de tres formas: o callback precisa retornar void ou uma cleanup function, o dependency array precisa conter valores validos, e funcoes async nao podem ser passadas diretamente como callback.
Na pratica, isso significa que o TypeScript te protege de padroes perigosos que funcionam em JavaScript puro mas geram problemas serios. A assinatura do useEffect e fixa: ele espera uma funcao que retorna void ou uma funcao de limpeza. Qualquer coisa diferente disso gera erro de compilacao.
// Assinatura do useEffect no TypeScript
useEffect(
effect: () => void | (() => void),
deps?: ReadonlyArray<unknown>
): void;
// O callback retorna:
// - void (sem cleanup)
// - () => void (com cleanup function)
Passo a passo: tipando useEffect corretamente
Vamos ver cada um desses pontos com codigo real. A ideia e que voce copie, adapte e use no seu projeto sem medo.
Exemplos praticos de useEffect tipado
useEffect basico com cleanup
O caso mais comum: voce adiciona um event listener e precisa remover quando o componente sai da tela. O TypeScript garante que a cleanup function nao retorne nada alem de void.
import { useEffect } from 'react';
function useWindowResize(callback: (width: number) => void) {
useEffect(() => {
const handleResize = () => {
callback(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup: remove o listener ao desmontar
return () => {
window.removeEventListener('resize', handleResize);
};
}, [callback]);
}
useEffect com async e AbortController
Da pra usar async dentro do useEffect sim, so nao pode passar a funcao async diretamente. O truque e criar uma funcao interna. E com AbortController voce cancela o fetch se o componente desmontar antes da resposta chegar.
import { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://api.example.com/users/${userId}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: User = await response.json();
setUser(data);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
return () => {
controller.abort();
};
}, [userId]);
if (loading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error}</p>;
return <p>{user?.name}</p>;
}
useEffect com interval tipado
Outro padrao recorrente: setInterval dentro do useEffect. O TypeScript reclama se voce nao tipar o retorno do setInterval direito. Olha como fica limpo:
import { useEffect, useState } from 'react';
function usePolling<T>(
fetcher: (signal: AbortSignal) => Promise<T>,
intervalMs: number
) {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
const controller = new AbortController();
const poll = async () => {
try {
const result = await fetcher(controller.signal);
setData(result);
} catch {
// Ignora erros de abort
}
};
poll(); // Primeira chamada imediata
const id: ReturnType<typeof setInterval> = setInterval(poll, intervalMs);
return () => {
clearInterval(id);
controller.abort();
};
}, [fetcher, intervalMs]);
return data;
}
Tipando dependencias com objetos complexos
Quando a dependencia e um objeto, o useEffect re-executa toda render porque a referencia muda. O TypeScript nao te avisa disso automaticamente, mas voce pode usar useMemo pra estabilizar:
import { useEffect, useMemo } from 'react';
interface Filters {
category: string;
sortBy: 'name' | 'date' | 'price';
page: number;
}
function ProductList({ category, sortBy, page }: Filters) {
// Estabiliza a referencia do objeto
const filters = useMemo<Filters>(
() => ({ category, sortBy, page }),
[category, sortBy, page]
);
useEffect(() => {
const controller = new AbortController();
const loadProducts = async () => {
const params = new URLSearchParams({
category: filters.category,
sort: filters.sortBy,
page: String(filters.page),
});
await fetch(`/api/products?${params}`, {
signal: controller.signal,
});
};
loadProducts();
return () => controller.abort();
}, [filters]);
return null;
}
Erros comuns e como evitar
Cuidado com esses erros
Passar funcao async diretamente no useEffect gera erro de tipo porque async retorna Promise, e o useEffect espera void. Outro erro classico: esquecer do AbortController e causar memory leaks com fetch pendente apos o componente desmontar. O TypeScript pega o primeiro, mas o segundo voce precisa cuidar na mao.
// ERRADO: async direto no useEffect
useEffect(async () => {
const data = await fetch('/api/users');
// TypeScript Error: Type 'Promise<void>' is not
// assignable to type 'void | (() => void)'
}, []);
// CERTO: funcao async interna
useEffect(() => {
const load = async () => {
const data = await fetch('/api/users');
};
load();
}, []);
// ERRADO: retornar valor no cleanup
useEffect(() => {
return 'cleanup'; // Erro: string nao e void | (() => void)
}, []);
// CERTO: retornar funcao no cleanup
useEffect(() => {
return () => {
console.log('componente desmontou');
};
}, []);
Mais um detalhe que pega muita gente: colocar um objeto literal no dependency array. O TypeScript aceita, mas o useEffect vai re-executar toda render porque {} !== {} por referencia. Use valores primitivos ou estabilize com useMemo.
Checklist: useEffect tipado sem erros
Checklist Final
Proximo passo: domine TypeScript na pratica
Projeto completo com TypeScript
Saber tipar useEffect e so o comeco. No CrazyStack voce constroi um projeto real do zero usando TypeScript, React e Node.js com todas as boas praticas que o mercado exige. Da tipagem de hooks ate a arquitetura do backend, tudo num unico projeto.