usePrevious — Sigue el valor anterior de cualquier estado o prop
Receta
import { useRef, useEffect } from "react";
/**
* usePrevious
* Devuelve el valor del render anterior.
* Devuelve `undefined` en el primer render.
*/
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
/**
* usePreviousDistinct
* Solo se actualiza cuando el valor realmente cambia
* (omite re-renders donde el valor permanece igual).
*/
function usePreviousDistinct<T>(
value: T,
isEqual: (a: T, b: T) => boolean = (a, b) => a === b
): T | undefined {
const prevRef = useRef<T | undefined>(undefined);
const currentRef = useRef<T>(value);
if (!isEqual(currentRef.current, value)) {
prevRef.current = currentRef.current;
currentRef.current = value;
}
return prevRef.current;
}Cuándo usarlo: Necesitas comparar valores actuales y anteriores para detectar la dirección del cambio, desencadenar animaciones, implementar deshacer u omitir efectos secundarios redundantes.
Ejemplo funcional
"use client";
import { useState } from "react";
// Detecta la dirección de la animación
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
const direction =
prevCount === undefined
? "inicial"
: count > prevCount
? "arriba"
: count < prevCount
? "abajo"
: "igual";
return (
<div>
<p>
Contador: {count} (fue: {prevCount ?? "N/D"})
</p>
<p>Dirección: {direction}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
</div>
);
}
// Detecta cambios de ruta
function RouteChangeDetector({ pathname }: { pathname: string }) {
const prevPathname = usePrevious(pathname);
useEffect(() => {
if (prevPathname && prevPathname !== pathname) {
console.log(`Navegado de ${prevPathname} a ${pathname}`);
// Rastrear vista de página, desplazarse hacia arriba, etc.
}
}, [pathname, prevPathname]);
return null;
}
// Deshacer simple para una entrada de texto
function UndoableInput() {
const [text, setText] = useState("");
const prevText = usePrevious(text);
const undo = () => {
if (prevText !== undefined) {
setText(prevText);
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Escribe algo..."
/>
<button onClick={undo} disabled={prevText === undefined}>
Deshacer
</button>
<p style={{ color: "#999" }}>Anterior: {prevText ?? "ninguno"}</p>
</div>
);
}Lo que esto demuestra:
- Dirección del contador: Compara el contador actual y anterior para determinar si aumentó, disminuyó o permaneció igual
- Cambios de ruta: Detecta navegación comparando la prop pathname en renders
- Deshacer de un nivel: Restaura el valor anterior de una entrada de texto
Análisis profundo
Cómo funciona
- Temporización de ref:
useRefpersiste en renders sin causar re-renders. EluseEffectse ejecuta después del render, por lo que durante el renderref.currentaún contiene el valor del render anterior. - Ciclo de render: En el render N,
ref.currentcontiene el valor del render N-1. Después de que se completa el render N, el efecto actualizaref.currental valor del render N. - Primer render: En el render inicial,
ref.currentesundefinedporque aún no existe ningún valor anterior. - usePreviousDistinct: Compara valores durante el render (no en un efecto) para omitir actualizaciones cuando el valor no ha cambiado realmente. Útil cuando un componente padre se re-renderiza sin cambiar la prop.
Parámetros y valores devueltos
usePrevious
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | T | — | El valor a rastrear |
| Devuelve | T o undefined | — | Valor del render anterior, o undefined en el primer render |
usePreviousDistinct
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | T | — | El valor a rastrear |
isEqual | (a: T, b: T) => boolean | === | Verificación de igualdad personalizada |
| Devuelve | T o undefined | — | Valor anterior distinto |
Variaciones
Con valor inicial: Evita undefined en el primer render:
function usePrevious<T>(value: T, initialValue: T): T {
const ref = useRef<T>(initialValue);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}Pila de historial: Rastrear múltiples valores anteriores para deshacer de varios niveles:
function usePreviousValues<T>(value: T, maxHistory: number = 10): T[] {
const historyRef = useRef<T[]>([]);
useEffect(() => {
historyRef.current = [value, ...historyRef.current].slice(0, maxHistory);
}, [value, maxHistory]);
return historyRef.current.slice(1); // Excluir valor actual
}Comparación de objetos: Para objetos, usa una función de comparación profunda:
const prevUser = usePreviousDistinct(user, (a, b) =>
JSON.stringify(a) === JSON.stringify(b)
);Notas de TypeScript
- El tipo de retorno
T | undefinedobliga al consumidor a manejar el caso del primer render. Usa la variación "con valor inicial" para evitar esto. - El genérico
Tse infiere del argumento, por lo que normalmente no se necesita un parámetro de tipo explícito. usePreviousDistinctacepta un comparador personalizado tipado como(a: T, b: T) => boolean.
Problemas
- Un render atrás — El valor devuelto es siempre del render anterior, no de la actualización de estado anterior. Si múltiples actualizaciones de estado ocurren en un render (por lotes), solo se captura el valor final renderizado. Solución: Esto es por diseño; el hook rastrea renders, no transiciones de estado.
- Referencias de objetos — Para objetos y arrays, "anterior" cambia en cada render si se crea una nueva referencia cada vez. Solución: Usa
usePreviousDistinctcon una comparación profunda, o memoriza el valor antes. - Ambigüedad de undefined — En el primer render, se devuelve
undefined. Siundefinedes un valor válido para tus datos, no puedes distinguir "sin anterior" de "anterior era undefined". Solución: Usa la variación "con valor inicial" o envuelve en un objeto{ hasPrevious, value }. - Deshacer es de un nivel —
usePrevioussolo rastrea un paso atrás. Solución: Usa la variación de pila de historial para deshacer de varios niveles. - Temporización del efecto con Strict Mode — En React 18 Strict Mode, los efectos se ejecutan dos veces en desarrollo. La ref se actualiza correctamente porque el segundo efecto sobrescribe el primero. Solución: No se requiere ninguna acción; el comportamiento de producción es correcto.
Alternativas
| Paquete | Nombre del hook | Notas |
|---|---|---|
usehooks-ts | usePrevious | Implementación idéntica |
@uidotdev/usehooks | usePrevious | Mínimo |
ahooks | usePrevious | Soporta comparación personalizada |
react-use | usePrevious | Simple basado en ref |
| Documentación de React | — | Recomendado como ejemplo de hook personalizado |
Preguntas frecuentes
¿Por qué usePrevious devuelve undefined en el primer render?
- La ref se inicializa a
undefinedy eluseEffectque la actualiza se ejecuta después del render. - En el primer render, no existe ningún valor anterior, por lo que
undefinedes el retorno correcto.
¿Cómo hace que la temporización de ref funcione en usePrevious?
- Durante el render N,
ref.currentaún contiene el valor del render N-1 porqueuseEffectaún no se ha ejecutado. - Después de que se completa el render N, el efecto actualiza
ref.currental valor actual, listo para el render N+1.
¿Cuál es la diferencia entre usePrevious y usePreviousDistinct?
usePreviousse actualiza en cada render, incluso si el valor no cambió.usePreviousDistinctsolo se actualiza cuando el valor realmente cambia (basado en una verificación de igualdad), omitiendo re-renders redundantes de componentes padres.
¿Cómo implementas deshacer de varios niveles en lugar de un nivel?
Usa una variación de pila de historial:
function usePreviousValues<T>(value: T, max = 10): T[] {
const historyRef = useRef<T[]>([]);
useEffect(() => {
historyRef.current = [value, ...historyRef.current].slice(0, max);
}, [value, max]);
return historyRef.current.slice(1);
}¿Cómo evitas devolver undefined en el primer render?
Usa la variación "con valor inicial":
function usePrevious<T>(value: T, initialValue: T): T {
const ref = useRef<T>(initialValue);
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}Problema: ¿Por qué usePrevious devuelve un nuevo "anterior" en cada render para objetos?
- Si el componente padre crea una nueva referencia de objeto en cada render,
usePreviouscaptura esa nueva referencia incluso si los datos son iguales. - Solución: usa
usePreviousDistinctcon una comparación profunda, o memoriza el valor antes conuseMemo.
Problema: ¿Puedes distinguir "sin valor anterior" de "anterior era undefined" usando el hook básico?
- No. Ambos casos devuelven
undefined, lo que es ambiguo. - Solución: usa la variación "con valor inicial" o envuelve el retorno en
{ hasPrevious: boolean, value: T }.
¿usePrevious rastrea renders o transiciones de estado?
- Rastrea renders. Si múltiples actualizaciones de estado se agrupan en un render, solo se captura el valor final renderizado.
- Esto es por diseño y coincide con el modelo de renderizado de React.
¿Cómo detecta el ejemplo de Counter la dirección del cambio?
- Compara
countconprevCount: sicount > prevCountla dirección es "arriba", si es menor es "abajo". - En el primer render,
prevCountesundefined, por lo que la dirección es "inicial".
¿Cómo maneja TypeScript el tipo de retorno de usePrevious?
- El tipo de retorno es
T | undefined, obligando al consumidor a manejar el caso del primer render. - El genérico
Tse infiere del argumento, por lo que normalmente no se necesita un parámetro de tipo explícito.
const prev = usePrevious(42);
// prev es number | undefined¿Cómo usas una función de igualdad personalizada con usePreviousDistinct en TypeScript?
El comparador se tipifica como (a: T, b: T) => boolean:
const prevUser = usePreviousDistinct(user, (a, b) =>
a.id === b.id && a.name === b.name
);¿Por qué usePreviousDistinct compara valores durante el render en lugar de en un efecto?
- Comparar durante el render permite actualizar refs sincronizadamente antes de que el componente devuelva JSX.
- Si usara un efecto, la actualización de ref se retrasaría hasta después del render, derrotando el propósito de omitir valores sin cambios.
Relacionado
- useRef — la primitiva subyacente
- useEffect — temporización de la actualización de ref
- useLocalStorage — persistir estado para deshacer entre sesiones