Safely persist React state to LocalStorage with SSR compatibility, cross-tab synchronization, and data validation.
use-local-storage-safe is a React hook designed to reliably manage state persistence in LocalStorage. It addresses common requirements such as maintaining state across sessions, ensuring data consistency between browser tabs via synchronization, handling potentially invalid stored data through validation, and supporting server-side rendering, all through a familiar useState-like interface.
- π‘οΈ Safe & Validated: Automatically validates stored data on initialization using your custom logic, preventing crashes from invalid or legacy data.
- π Cross-Tab Sync: Effortlessly synchronizes state across multiple browser tabs or windows using the native StorageEvent API (can be disabled).
- β SSR Compatible: Works seamlessly with server-side rendering frameworks (Next.js, Astro, Remix, etc.) by safely returning the default value on the server.
- βοΈ TypeScript Native: Written in TypeScript with full type safety for keys and values.
- π§ Customizable: Provides options for custom serialization (
stringify), deserialization (parse), error logging (log), and error suppression (silent). - π Lightweight: Minimal footprint with zero dependencies besides React itself.
- β Simple API: Designed to be a drop-in replacement for
useStatefor persistent state.
npm i use-local-storage-safe # npmyarn add use-local-storage-safe # yarnpnpm i use-local-storage-safe # pnpm- Reliable Persistence: Simple
useState-like interface for data that survives page reloads. - Data Integrity: Protect your application from unexpected errors caused by malformed data in
localStorageusing thevalidateInitoption. - Seamless User Experience: Keep the UI consistent across all open tabs with the built-in
syncfeature. - Universal Compatibility: Works flawlessly in both client-side and server-side rendered React applications (React
>=16.8.0). - Modern Tooling: Supports both ESM (ECMAScript modules) and CJS (CommonJS) formats.
import { useLocalStorageSafe } from 'use-local-storage-safe'
export default function NameComponent() {
const [userName, setUserName] = useLocalStorageSafe('name-storage-key', 'default-name')
}import { useLocalStorageSafe } from 'use-local-storage-safe'
// data could be validated with plain JS or any other library
import { z } from "zod";
const User = z.object({
firstName: z.string().min(1).max(18),
lastName: z.string().min(1).max(18),
email: z.string().email(),
});
type User = z.infer<typeof User>
export default function UserComponent() {
const [user, setUser] = useLocalStorageSafe<User>(
"user-storage-key",
{
firstName: "example name",
lastName: "example last name",
email: "[email protected]",
},
// Options object
{
// Validate stored data on hook initialization using a Zod schema
validateInit: (value) => User.safeParse(value).success,
// Optional: Custom logger (defaults to console.log)
// log: (message) => console.warn('LocalStorage:', message),
// Optional: Disable cross-tab sync (defaults to true)
// sync: false,
// Optional: Throw errors instead of logging them silently (defaults to true)
// silent: false,
// Optional: Custom serialization (e.g., for Map, Set, Date)
// stringify: (value) => SuperJSON.stringify(value),
// parse: (storedValue) => SuperJSON.parse(storedValue),
}
);
return (
<div>
<p>First Name: {user.firstName}</p>
<p>Last Name: {user.lastName}</p>
<p>Email: {user.email}</p>
<button
onClick={() =>
setUser({ firstName: "U", lastName: "Nu", email: "[email protected]" })
}
>
Set User
</button>
</div>
);
}Overloads:
// When defaultValue is provided, T is guaranteed
function useLocalStorageSafe<T>(
key: string,
defaultValue: T,
options?: Options<T>
): [T, Dispatch<SetStateAction<T>>];
// When defaultValue is potentially undefined
function useLocalStorageSafe<T>(
key: string,
defaultValue?: T,
options?: Options<T>
): [T | undefined, Dispatch<SetStateAction<T | undefined>>];Options Interface:
interface Options<T> {
/** Custom stringify function (e.g., JSON.stringify, SuperJSON.stringify). Defaults to JSON.stringify. */
stringify?: (value: unknown) => string;
/** Custom parse function (e.g., JSON.parse, SuperJSON.parse). Must return the expected type T. Defaults to JSON.parse. */
parse?: (stringValue: string) => T;
/** Custom logging function for errors. Defaults to console.log. */
log?: (message: unknown) => void;
/** Function to validate the stored value on initial load. Return true if valid, false otherwise. If false, the stored item is removed, and the defaultValue is used (if provided). */
validateInit?: (value: T) => boolean;
/** Synchronize state across browser tabs/windows via StorageEvent. Defaults to true. */
sync?: boolean;
/** Suppress localStorage access errors (e.g., QuotaExceededError) and log them instead. Defaults to true. */
silent?: boolean;
}key: string(Required) - A unique key to identify the value inlocalStorage.defaultValue: T | undefined(Optional) - The initial value to use if nothing is found inlocalStoragefor the givenkey. Also used during server-side rendering.options: Options<T>(Optional) - An object to customize behavior:stringify: Customize how your stateTis converted to a string for storage. Useful for types beyond simple JSON (likeMap,Set,Date).parse: Customize how the string retrieved from storage is converted back to your state typeT. Must correspond to yourstringifylogic.log: Provide a custom function (likeconsole.warn,console.error, or a custom logger) to handle errors caught during storage access or parsing.validateInit: Provide a function that receives the parsed value fromlocalStorageon hook initialization. If it returnsfalse, the invalid item is removed fromlocalStorage, and thedefaultValueis used instead. This prevents crashes from malformed or outdated data structures.sync: Set tofalseto prevent the hook from listening toStorageEventand updating its state when the same key is modified in another browser tab or window.silent: Set tofalseto throw errors encountered duringlocalStorage.setItemorlocalStorage.getItem(e.g., storage quota exceeded, security restrictions) instead of catching and logging them.
Returns a tuple similar to React.useState:
StoredValue: T | undefined- The current value of the state. It will beTif adefaultValuewas provided or if a valid value exists in storage. It can beundefinedif nodefaultValuewas given and nothing is in storage (or if the stored value is explicitlyundefined).setValue: Dispatch<SetStateAction<T | undefined>>- A function to update the state. It accepts either the new value or a function that receives the previous value and returns the new value.