Custom hooks, composición de hooks, y patrones como useReducer con Context para manejar estado complejo sin Redux.
Custom Hooks: extrayendo lógica reutilizable
La regla de oro: si tienes lógica con estado que aparece en más de un componente, es candidata a ser un custom hook.
useFetch — el clásico
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Uso:
function UserProfile({ id }: { id: string }) {
const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
if (loading) return <Spinner />;
if (error) return <Error />;
return <Profile user={data!} />;
}
useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initial;
} catch {
return initial;
}
});
const set = useCallback((val: T | ((prev: T) => T)) => {
setValue(prev => {
const next = typeof val === 'function' ? (val as (p: T) => T)(prev) : val;
localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
return [value, set] as const;
}
useReducer + Context: estado global sin Redux
Para estado global moderadamente complejo, useReducer con Context es suficiente y no requiere dependencias externas.
// store/theme.tsx
type Theme = 'dark' | 'light';
type Action = { type: 'TOGGLE_THEME' } | { type: 'SET_THEME'; payload: Theme };
function reducer(state: Theme, action: Action): Theme {
switch (action.type) {
case 'TOGGLE_THEME': return state === 'dark' ? 'light' : 'dark';
case 'SET_THEME': return action.payload;
default: return state;
}
}
const ThemeContext = createContext<{
theme: Theme;
dispatch: Dispatch<Action>;
} | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, dispatch] = useReducer(reducer, 'dark');
return (
<ThemeContext.Provider value={{ theme, dispatch }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
return ctx;
}
Composición de hooks
Los hooks se componen naturalmente — puedes construir hooks complejos a partir de simples:
function useUserData(userId: string) {
const { data: user, loading: userLoading } = useFetch<User>(`/api/users/${userId}`);
const { data: posts, loading: postsLoading } = useFetch<Post[]>(`/api/users/${userId}/posts`);
return {
user,
posts,
loading: userLoading || postsLoading,
};
}
useCallback y useMemo — cuándo usarlos
La regla: úsalos cuando el costo de recomputar es mayor que el costo de comparar.
// ✅ Sí usar useMemo — cálculo costoso
const sortedItems = useMemo(
() => items.sort((a, b) => b.score - a.score),
[items]
);
// ✅ Sí usar useCallback — función pasada a componente memoizado
const handleSubmit = useCallback(
(data: FormData) => {
mutate(data);
},
[mutate]
);
// ❌ No necesario — el cálculo es trivial
const doubled = useMemo(() => count * 2, [count]);
useRef más allá de los DOM refs
useRef almacena valores mutables que no deben triggear re-renders:
function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef(callback);
// Actualizar la referencia sin re-render
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
Conclusión
Los hooks cambian la forma de pensar en React: en lugar de “qué lifecycle method uso aquí”, piensas en “qué efecto tiene este valor”. La composición es el superpoder real — hooks pequeños y enfocados que se combinan en comportamientos complejos.
El patrón useReducer + Context cubre el 80% de los casos donde antes hubieras alcanzado para Redux. Empieza simple y añade complejidad solo cuando la necesites.