Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useCallback } from "react";
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// El inicializador lazy lee del almacenamiento solo una vez
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = localStorage.getItem(key);
return item !== null ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
// Persiste en localStorage cuando el valor o la clave cambian
useEffect(() => {
if (typeof window === "undefined") return;
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch {
// Cuota de almacenamiento excedida o no disponible
}
}, [key, storedValue]);
// Sincroniza entre pestañas a través del evento storage
useEffect(() => {
if (typeof window === "undefined") return;
const handleStorage = (e: StorageEvent) => {
if (e.key !== key) return;
if (e.newValue === null) {
setStoredValue(initialValue);
} else {
try {
setStoredValue(JSON.parse(e.newValue) as T);
} catch {
// Ignora JSON malformado
}
}
};
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, [key, initialValue]);
// Setter que coincide con la firma de useState (valor o fn actualizadora)
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue =
value instanceof Function ? value(prev) : value;
return nextValue;
});
},
[]
);
// Elimina la clave del almacenamiento y restablece a inicial
const remove = useCallback(() => {
if (typeof window !== "undefined") {
localStorage.removeItem(key);
}
setStoredValue(initialValue);
}, [key, initialValue]);
return [storedValue, setValue, remove];
}Cuándo usarlo: Deseas que el estado del componente sobreviva a las actualizaciones de página, y opcionalmente se mantenga sincronizado en las pestañas del navegador, sin necesidad de una biblioteca externa.
"use client";
function ThemeToggle() {
const [theme, setTheme, removeTheme] = useLocalStorage<"light" | "dark">(
"app-theme",
"light"
);
return (
<div>
<p>Tema actual: {theme}</p>
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
Alternar tema
</button>
<button onClick={removeTheme}>Restablecer predeterminado</button>
</div>
);
}
function FormDraft() {
const [draft, setDraft] = useLocalStorage("form-draft", {
name: "",
email: "",
});
return (
<form>
<input
value={draft.name}
onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))}
placeholder="Name"
/>
<input
value={draft.email}
onChange={(e) => setDraft((d) => ({ ...d, email: e.target.value }))}
placeholder="Email"
/>
<p>Borrador guardado automáticamente en localStorage</p>
</form>
);
}Lo que esto demuestra:
"light" | "dark"(prev) => next como en useStateremove para borrar la clave y restablecer el estadostorageuseState lee de localStorage solo en el primer renderizado, evitando lecturas redundantes en cada re-renderizado.window está protegido por verificaciones typeof window === "undefined", de modo que el hook devuelve initialValue durante la renderización del lado del servidor sin desincronización de hidratación.JSON.stringify y se deserializan con JSON.parse. Esto maneja primitivos, arrays y objetos simples.storage se dispara en otras pestañas cuando la misma clave cambia. El listener actualiza el estado local para que coincida.(prev) => next, reflejando la API de useState.| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
key | string | - | La clave de localStorage |
initialValue | T | - | Fallback cuando la clave falta o en SSR |
| Índice de retorno | Tipo | Descripción |
|---|---|---|
[0] | T | Valor actual |
[1] | (value: T or ((prev: T) => T)) => void | Setter (valor o función actualizadora) |
[2] | () => void | Elimina la clave y restablece a inicial |
Con expiración: Añade un TTL almacenando { value, expiresAt } y verificando al leer:
const item = JSON.parse(raw);
if (item.expiresAt && Date.now() > item.expiresAt) {
localStorage.removeItem(key);
return initialValue;
}
return item.value;Con serializador personalizado: Acepta opciones serialize y deserialize para datos no JSON (por ejemplo, superjson para objetos Date, Map, Set).
Variante de sessionStorage: Cambia localStorage por sessionStorage - la API es idéntica, pero los datos se borran cuando se cierra la pestaña.
T fluye desde initialValue al estado almacenado y al setter, proporcionando inferencia de tipo completa."light" | "dark" estrechan la entrada del setter automáticamente.useLocalStorage<User>("user", defaultUser).undefined, y objetos circulares no pueden sobrevivir a JSON.stringify. Solución: Solo almacena datos serializables simples. Usa superjson para Date, Map, Set.storage. Solución: Este hook maneja las actualizaciones de la misma pestaña a través de setState; la sincronización entre pestañas se maneja mediante el event listener.myapp:theme.initialValue pero el cliente lee un valor diferente del almacenamiento, ocurre una desincronización. Solución: El inicializador lazy solo se ejecuta en el cliente. Para frameworks SSR, el renderizado inicial usa initialValue, luego se actualiza después de la hidratación.| Paquete | Nombre del hook | Notas |
|---|---|---|
usehooks-ts | useLocalStorage | Popular, API similar |
@uidotdev/usehooks | useLocalStorage | Mínimal, bien probado |
ahooks | useLocalStorageState | Soporta serializador personalizado |
jotai | atomWithStorage | Persistencia basada en átomo |
zustand | persist middleware | Persistencia a nivel de almacén |
storage se dispara en otras pestañas cuando la misma clave se escribe en localStorage.setState directamente.El inicializador lazy (forma de callback de useState) se ejecuta solo en el primer renderizado. Leer localStorage en cada renderizado sería ineficaz ya que el valor almacenado solo necesita leerse una vez al montar.
JSON.stringify.undefined también se descarta (se convierte en null).Date, Map, y Set pierden sus tipos (se convierten en strings/arrays). Usa superjson para estos.Los navegadores limitan localStorage a alrededor de 5 MB por origen. Cuando se alcanza el límite, setItem lanza una excepción. El hook envuelve la escritura en un try/catch para que la aplicación no se bloquee, pero los datos no se persisten silenciosamente.
Prefija tus claves con un espacio de nombres específico de la aplicación:
const [theme, setTheme] = useLocalStorage("myapp:theme", "light");El servidor renderiza con initialValue, pero el cliente lee un valor diferente del almacenamiento al montar. El inicializador lazy se ejecuta solo en el cliente, causando una breve desincronización. Esto es lo esperado; para UI crítica, considera retrasar el renderizado hasta que se complete la hidratación.
JSON.parse convierte las cadenas Date de vuelta a cadenas simples, no a objetos Date. Usa un serializador personalizado como superjson que preserve los tipos, o rehidrata manualmente las fechas después de leer.
La callback setValue verifica value instanceof Function. Si es verdadero, llama a la función con el estado anterior. De lo contrario, usa el valor directamente. Esto refleja la API de useState.
El genérico T se infiere de initialValue. Por ejemplo, useLocalStorage("theme", "light") infiere T como string. Para tipos de unión, pasa el genérico explícitamente:
useLocalStorage<"light" | "dark">("theme", "light");Sí. La API de sessionStorage es idéntica a localStorage. Cambia cada llamada localStorage por sessionStorage. La única diferencia es que los datos se borran cuando se cierra la pestaña del navegador.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥