Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Estas receitas de skill são projetadas para Claude Code, mas também funcionam com outros agentes de codificação de IA que suportam arquivos de skill/instrução.
O conteúdo completo do SKILL.md que você pode copiar para .claude/skills/custom-hooks-crafting/SKILL.md:
---
name: custom-hooks-crafting
description: "Criação de hooks React personalizados e reutilizáveis com TypeScript adequado. Use quando solicitado para: criar um hook, hook personalizado, extrair hook, hook reutilizável, composição de hooks, testar um hook."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Criação de Hooks Personalizados
Você é um especialista em criar hooks React personalizados. Cada hook que você cria segue princípios de design rigorosos, é totalmente tipado, seguro para SSR e testável.
## Regras de Design de Hooks
1. **Responsabilidade única** - Cada hook faz exatamente uma coisa.
2. **Nome começa com `use`** - Sempre prefixe com `use` (o React impõe isso).
3. **Convenções de tipo de retorno:**
- Valor único: retorne o valor diretamente.
- Valor + setter: retorne uma tupla `[valor, setter]` (como useState).
- Múltiplos valores relacionados: retorne um objeto `{ valor, loading, error }`.
4. **Aceite configuração como um objeto** - Quando um hook recebe mais de 2 parâmetros, use um objeto de opções.
5. **Forneça padrões sensatos** - Cada opção deve ter um valor padrão.
6. **Limpe após você mesmo** - Sempre retorne funções de limpeza de useEffect.
7. **Segurança SSR** - Verifique `typeof window !== "undefined"` antes de acessar APIs do navegador.
8. **Referências estáveis** - Envolva funções retornadas em useCallback, envolva objetos retornados em useMemo.
## Template de Criação de Hook
```tsx
import \{ useState, useEffect, useCallback, useRef \} from "react";
interface UseMyHookOptions \{
/** Descrição da opção */
enabled?: boolean;
/** Descrição da opção */
interval?: number;
\}
interface UseMyHookReturn \{
/** Descrição do valor de retorno */
data: string | null;
/** Descrição do valor de retorno */
loading: boolean;
/** Descrição do valor de retorno */
error: Error | null;
/** Descrição do valor de retorno */
reset: () => void;
\}
/**
* Descrição do que o hook faz e quando usá-lo.
*
* @example
* const \{ data, loading, error \} = useMyHook(\{ enabled: true \});
*/
export function useMyHook(options: UseMyHookOptions = \{\}): UseMyHookReturn \{
const \{ enabled = true, interval = 1000 \} = options;
const [data, setData] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Use ref para valores necessários em efeitos, mas que não devem disparar re-execuções
const intervalRef = useRef(interval);
intervalRef.current = interval;
useEffect(() => \{
if (!enabled) return;
let cancelled = false;
setLoading(true);
async function fetchData() \{
try \{
const result = await someAsyncOperation();
if (!cancelled) \{
setData(result);
setLoading(false);
\}
\} catch (err) \{
if (!cancelled) \{
setError(err instanceof Error ? err : new Error(String(err)));
setLoading(false);
\}
\}
\}
fetchData();
return () => \{ cancelled = true; \};
\}, [enabled]);
const reset = useCallback(() => \{
setData(null);
setLoading(false);
setError(null);
\}, []);
return \{ data, loading, error, reset \};
\}export function useDebounce<T>(value: T, delay: number): T \{
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => \{
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
\}, [value, delay]);
return debouncedValue;
\}export function useMediaQuery(query: string): boolean \{
const [matches, setMatches] = useState(false);
useEffect(() => \{
if (typeof window === "undefined") return;
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
\}, [query]);
return matches;
\}export function useClickOutside<T extends HTMLElement>(
handler: () => void
): React.RefObject<T | null> \{
const ref = useRef<T | null>(null);
useEffect(() => \{
const listener = (event: MouseEvent | TouchEvent) => \{
if (!ref.current || ref.current.contains(event.target as Node)) return;
handler();
\};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => \{
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
\};
\}, [handler]);
return ref;
\}export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] \{
const [storedValue, setStoredValue] = useState<T>(() => \{
if (typeof window === "undefined") return initialValue;
try \{
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
\} catch \{
return initialValue;
\}
\});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => \{
setStoredValue((prev) => \{
const next = value instanceof Function ? value(prev) : value;
if (typeof window !== "undefined") \{
window.localStorage.setItem(key, JSON.stringify(next));
\}
return next;
\});
\},
[key]
);
return [storedValue, setValue];
\}interface UseIntersectionOptions \{
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
\}
export function useIntersectionObserver<T extends HTMLElement>(
options: UseIntersectionOptions = \{\}
): [React.RefObject<T | null>, boolean] \{
const \{ threshold = 0, rootMargin = "0px", triggerOnce = false \} = options;
const ref = useRef<T | null>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => \{
const element = ref.current;
if (!element || typeof IntersectionObserver === "undefined") return;
const observer = new IntersectionObserver(
([entry]) => \{
setIsVisible(entry.isIntersecting);
if (entry.isIntersecting && triggerOnce) \{
observer.unobserve(element);
\}
\},
\{ threshold, rootMargin \}
);
observer.observe(element);
return () => observer.disconnect();
\}, [threshold, rootMargin, triggerOnce]);
return [ref, isVisible];
\}export function usePrevious<T>(value: T): T | undefined \{
const ref = useRef<T | undefined>(undefined);
useEffect(() => \{
ref.current = value;
\});
return ref.current;
\}Extraia um hook personalizado quando:
useState + useEffect aparece em 2+ componentes.NÃO extraia um hook quando:
useState (adiciona indireção sem benefício).import \{ renderHook, act, waitFor \} from "@testing-library/react";
import \{ useMyHook \} from "./useMyHook";
describe("useMyHook", () => \{
it("retorna o estado inicial", () => \{
const \{ result \} = renderHook(() => useMyHook());
expect(result.current.data).toBeNull();
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
\});
it("atualiza o estado na ação", () => \{
const \{ result \} = renderHook(() => useMyHook(\{ enabled: true \}));
act(() => \{
result.current.reset();
\});
expect(result.current.data).toBeNull();
\});
it("lida com operações assíncronas", async () => \{
const \{ result \} = renderHook(() => useMyHook(\{ enabled: true \}));
await waitFor(() => \{
expect(result.current.loading).toBe(false);
\});
expect(result.current.data).toBeDefined();
\});
it("limpa na desmontagem", () => \{
const \{ unmount \} = renderHook(() => useMyHook());
// Verifica se não há erros ou avisos na desmontagem
expect(() => unmount()).not.toThrow();
\});
it("responde a mudanças de opção", () => \{
const \{ result, rerender \} = renderHook(
(\{ enabled \}) => useMyHook(\{ enabled \}),
\{ initialProps: \{ enabled: false \} \}
);
expect(result.current.loading).toBe(false);
rerender(\{ enabled: true \});
expect(result.current.loading).toBe(true);
\});
\});
## Exemplo de Trabalho
### Exemplo 1: Usuário pergunta "Crie um hook para copiar texto para a área de transferência"
**Prompt do usuário:** "Preciso de um hook que copie texto para a área de transferência e mostre um estado de sucesso por 2 segundos."
**A resposta guiada pela skill produziria:**
```tsx
// hooks/use-clipboard.ts
import { useState, useCallback, useRef } from "react";
interface UseClipboardOptions {
/** Quanto tempo mostrar o estado copiado em ms */
successDuration?: number;
}
interface UseClipboardReturn {
copied: boolean;
copy: (text: string) => Promise<void>;
error: Error | null;
}
export function useClipboard(
options: UseClipboardOptions = {}
): UseClipboardReturn {
const { successDuration = 2000 } = options;
const [copied, setCopied] = useState(false);
const [error, setError] = useState<Error | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const copy = useCallback(
async (text: string) => {
if (typeof navigator === "undefined" || !navigator.clipboard) {
setError(new Error("API da área de transferência não disponível"));
return;
}
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setError(null);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setCopied(false), successDuration);
} catch (err) {
setError(err instanceof Error ? err : new Error("Falha ao copiar"));
setCopied(false);
}
},
[successDuration]
);
return { copied, copy, error };
}
// hooks/use-clipboard.test.ts
import { renderHook, act } from "@testing-library/react";
import { useClipboard } from "./use-clipboard";
// Mock da API da área de transferência
Object.assign(navigator, {
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
});
describe("useClipboard", () => {
it("copia texto e define copied como true", async () => {
const { result } = renderHook(() => useClipboard());
await act(async () => {
await result.current.copy("hello");
});
expect(result.current.copied).toBe(true);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("hello");
});
it("reseta copied após successDuration", async () => {
vi.useFakeTimers();
const { result } = renderHook(() =>
useClipboard({ successDuration: 1000 })
);
await act(async () => {
await result.current.copy("hello");
});
expect(result.current.copied).toBe(true);
act(() => vi.advanceTimersByTime(1000));
expect(result.current.copied).toBe(false);
vi.useRealTimers();
});
});A resposta guiada pela skill: Identificaria o padrão compartilhado de estado/efeito, o extrairia seguindo o template, adicionaria tipos TypeScript adequados, garantiria a segurança SSR e forneceria testes.
Esta skill fornece:
mkdir -p .claude/skills/custom-hooks-crafting
# Cole o conteúdo da Receita em .claude/skills/custom-hooks-crafting/SKILL.mduseEffect é executada antes de cada re-execução, não apenas na desmontagem - Projete funções de limpeza para lidar com ambos os casos.useState são executadas apenas uma vez - useState(() => expensiveComputation()) é diferente de useState(expensiveComputation()).use() no React 19 pode.| Abordagem | Quando Usar |
|---|---|
| Funções utilitárias simples | Lógica que não precisa de hooks do React |
| Componentes de ordem superior | Envolver comportamento em torno de um componente (padrão legado) |
| Render props | Compartilhar comportamento com controle de renderização (padrão legado) |
| Seletores Zustand | Estado compartilhado entre componentes (não apenas lógica compartilhada) |
useState + useEffect aparece em 2+ componentes.useState.use para aplicar as Regras de Hooks (sem chamadas condicionais, sem chamadas em loops).[valor, setter] (como useState).{ valor, loading, error }.function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
// T é inferido de initialValue
}typeof window !== "undefined" antes de acessar APIs do navegador como localStorage, navigator ou matchMedia.false para useMediaQuery).useCallback, a referência da função muda a cada renderização.import { renderHook, act } from "@testing-library/react";
import { useMyHook } from "./useMyHook";
it("retorna o estado inicial", () => {
const { result } = renderHook(() => useMyHook());
expect(result.current.data).toBeNull();
});
it("atualiza na ação", () => {
const { result } = renderHook(() => useMyHook());
act(() => result.current.reset());
expect(result.current.data).toBeNull();
});useState(() => expensiveComputation()) executa a função apenas uma vez na renderização inicial (inicializador preguiçoso).useState(expensiveComputation()) executa a função em cada renderização, desperdiçando computação.handlerRef.current = handler.handlerRef.current(...) em vez de handler(...).function useClickOutside<T extends HTMLElement>(
handler: () => void
): React.RefObject<T | null> {
const ref = useRef<T | null>(null);
// anexa listeners do documento, verifica ref.current.contains
return ref;
}Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥