Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
useRef persiste en renders sin causar re-renders. El useEffect se ejecuta después del render, por lo que durante el render ref.current aún contiene el valor del render anterior.ref.current contiene el valor del render N-1. Después de que se completa el render N, el efecto actualiza ref.current al valor del render N.ref.current es undefined porque aún no existe ningún valor anterior.| 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 |
| 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 |
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)
);T | undefined obliga al consumidor a manejar el caso del primer render. Usa la variación "con valor inicial" para evitar esto.T se infiere del argumento, por lo que normalmente no se necesita un parámetro de tipo explícito.usePreviousDistinct acepta un comparador personalizado tipado como (a: T, b: T) => boolean.usePreviousDistinct con una comparación profunda, o memoriza el valor antes.undefined. Si undefined es 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 }.usePrevious solo rastrea un paso atrás. Solución: Usa la variación de pila de historial para deshacer de varios niveles.| 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 |
undefined y el useEffect que la actualiza se ejecuta después del render.undefined es el retorno correcto.ref.current aún contiene el valor del render N-1 porque useEffect aún no se ha ejecutado.ref.current al valor actual, listo para el render N+1.usePrevious se actualiza en cada render, incluso si el valor no cambió.usePreviousDistinct solo se actualiza cuando el valor realmente cambia (basado en una verificación de igualdad), omitiendo re-renders redundantes de componentes padres.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);
}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;
}usePrevious captura esa nueva referencia incluso si los datos son iguales.usePreviousDistinct con una comparación profunda, o memoriza el valor antes con useMemo.undefined, lo que es ambiguo.{ hasPrevious: boolean, value: T }.count con prevCount: si count > prevCount la dirección es "arriba", si es menor es "abajo".prevCount es undefined, por lo que la dirección es "inicial".T | undefined, obligando al consumidor a manejar el caso del primer render.T se infiere del argumento, por lo que normalmente no se necesita un parámetro de tipo explícito.const prev = usePrevious(42);
// prev es number | undefinedEl comparador se tipifica como (a: T, b: T) => boolean:
const prevUser = usePreviousDistinct(user, (a, b) =>
a.id === b.id && a.name === b.name
);Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥