El Síntoma: Componentes que Hacen Demasiado
Si llevas un tiempo trabajando con React, seguro que esta situación te suena familiar: un componente que empieza simple, pero al que poco a poco le añades un estado para el loading, otro para el error, un useEffect
para cargar datos, otra lógica para guardar en el navegador... y de repente, tienes 50 líneas de código solo para gestionar la lógica, antes de escribir una sola línea de JSX.
Este es el momento exacto en el que dejas de ser solo un consumidor de hooks (`useState`, `useEffect`) y sientes la necesidad de convertirte en un creador. La respuesta a este desorden es una de las características más potentes de React: los Custom Hooks.
¿Por qué crear tus propios Hooks? La Magia de la Abstracción
Crear tus propios hooks no es un truco avanzado, es una práctica fundamental para escribir código limpio y escalable. Las ventajas son inmediatas:
- Reutilización (DRY): No escribas la misma lógica de fetching de datos en diez componentes distintos. Escríbela una vez en un hook y úsala en todas partes.
- Legibilidad y Abstracción: Tu componente deja de preocuparse por cómo se obtienen los datos y solo se preocupa de qué hacer con ellos. La complejidad queda oculta dentro del hook.
- Separación de Responsabilidades: Los componentes se centran en la UI, y los hooks se centran en la lógica de estado y los efectos secundarios.
- Facilidad para Testear: Es mucho más sencillo probar la lógica de un hook de forma aislada que la de un componente completo.
Tutorial: Creando Nuestros 3 Primeros Custom Hooks
La regla de oro es simple: un Custom Hook es solo una función de JavaScript cuyo nombre empieza por "use"
y que puede llamar a otros hooks. Vamos a la práctica.
1. El Básico: `useToggle`
Empecemos con algo sencillo para entender el patrón. Un hook para manejar un estado booleano (verdadero/falso).
// hooks/useToggle.js
import { useState, useCallback } from 'react';
export const useToggle = (initialState = false) => {
const [state, setState] = useState(initialState);
// Usamos useCallback para que la función no se recree en cada render
const toggle = useCallback(() => {
setState(prevState => !prevState);
}, []);
return [state, toggle];
};
// Cómo se usa en un componente:
// const [isVisible, toggleVisibility] = useToggle(false);
¡Felicidades! Acabas de crear un hook que abstrae la lógica de un interruptor. Simple, pero increíblemente reutilizable.
2. El Esencial: `useFetch`
Esta es la joya de la corona. Casi todas las aplicaciones necesitan obtener datos de una API. En lugar de repetir la lógica de loading, error y data en cada componente, la centralizamos.
// hooks/useFetch.js
import { useState, useEffect } from 'react';
export const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Evita que se actualice el estado si el componente se desmonta
const abortController = new AbortController();
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error('La respuesta de la red no fue correcta');
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
// Función de limpieza para cancelar el fetch si el componente se desmonta
return () => abortController.abort();
}, [url]); // Se ejecuta de nuevo solo si la URL cambia
return { data, loading, error };
};
Ahora, mira qué limpio queda tu componente:
// components/UserProfile.js
import { useFetch } from '../hooks/useFetch';
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Hemos pasado de unas 20 líneas de lógica a una sola línea declarativa. Esto es espectacular.
3. El Avanzado con TypeScript: `useLocalStorage`
Vamos a crear un hook que sincronice un estado de React con el localStorage
del navegador, pero de forma segura con TypeScript usando genéricos.
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
// El tipo genérico nos permite usar este hook con cualquier tipo de dato
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Cómo se usa para guardar un tema oscuro (string)
// const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
// Cómo se usa para guardar datos de usuario (objeto)
// const [user, setUser] = useLocalStorage('user', null);
Este hook no solo abstrae la lógica, sino que es robusto, seguro y se adapta a cualquier tipo de dato que necesites persistir.
Tu Nuevo Superpoder en React
Pasar de usar hooks a crearlos es como pasar de seguir recetas a escribir tu propio libro de cocina. Te da el poder de construir soluciones a medida, limpiar tus componentes y, en última instancia, escribir mejor software.
La próxima vez que te encuentres copiando y pegando un bloque de lógica de estado, detente. Respira hondo y pregúntate: "¿Podría esto ser un hook?". La respuesta, muy probablemente, será que sí.