Library / SDK
astoilkov/use-local-storage-state avatar
astoilkov/use-local-storage-state

use-local-storage-state is a 677 byte React hook for localStorage

React hook that persists data in localStorage

1,233 stars44 forksTypeScriptMIT

At a glance

What is it?
This MIT licensed hook gives a React component the useState interface backed by localStorage, plus cross-tab sync through the Window storage event, an in-memory fallback when storage throws, and a third tuple element exposing removeItem and isPersistent. It is a small package with sharp edges around server rendering and key collisions.
Who is it for?
Adopt use-local-storage-state when you want per component persistence with the useState shape and no third party state manager, and skip it when your data must be shared across components, must survive a cache quota failure, or needs a schema.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 99 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A hook that behaves like useState and writes to localStorage

use-local-storage-state is a single React hook whose whole job is to make a piece of component state survive a page reload. The API mirrors useState. You pass a key and an options object with a defaultValue, you get back a value and a setter, and the setter behaves the way you would expect from a state setter.

The difference is where the state lives. Instead of React's internal state, the value is written to localStorage under the key you named, so the next mount of the component reads it back.

The project presents the package as production ready, and the README quotes a bundle size of 677 B brotli'd. The repository layout supports that claim. The published files are only index.js, index.d.ts and the compiled files under src/, the package is marked sideEffects false, and the build is a plain tsc invocation. The package is published as an ES module with a default export and separate type declarations.

It is a package with no dependencies beyond React itself, and React and React DOM are peer dependencies at version 18 or newer. That constraint is the single most important line in the package.json for anyone planning to use it, and it is worth reading before anything else.

The third tuple element is where the useful behaviour hides

The hook returns three values, not two. The first two behave like useState. The third is an object with two properties, and it is the reason the API is not just a thin wrapper.

removeItem() calls localStorage.removeItem(key) and resets the hook to its default state, returning the component to the same condition it was in when the hook was first created. That is not the same as setting the value to the default, because the key is also removed from storage rather than overwritten with the default.

isPersistent is a boolean that returns false when localStorage is throwing an error and the data is being held only in memory. The README describes it as a way to notify the user that their data will not be persisted, and it links to the case where localStorage is not available at all.

Between them these two properties cover the two situations a naive implementation gets wrong. A hand written useEffect that writes to localStorage typically leaves a stale key behind when a user logs out, and it typically fails silently when the browser refuses to store anything, in private browsing or when the quota is full. This package gives you both an explicit reset and an explicit signal.

Cross-tab sync depends on the Window storage event

The hook subscribes to the browser's Window storage event, which is how a change in one tab is propagated to other tabs, windows and iframes on the same origin. The default is on.

If you set the storageSync option to false, the hook stops subscribing to that event, and updates are no longer synchronized across tabs, windows and iframes. The option exists for the case where a shared value is deliberately kept local to a tab, such as a per-tab UI preference that you would not want to jump to another tab mid-interaction.

This is the one part of the library that depends on browser behaviour rather than on your code, and it has a consequence worth planning for. A tab that is closed and reopened reads whatever is currently in localStorage, which may be a value written by a different tab in the meantime. The storage event keeps live tabs in step, but it does not make the stored value a source of truth for anything server side.

The README also warns separately about key collisions: be careful with names, because it is possible to access a property that was already in localStorage and was created elsewhere in the codebase or in an old version of the application. A renamed feature that reuses an old key will silently read the wrong value.

The in-memory fallback is a feature and a warning sign

When localStorage throws an error and cannot store the data, the hook keeps the value in memory instead of crashing. The data is then lost when the page unloads, which is exactly why isPersistent exists.

The README frames the fallback as helpful, and it is, because a quota error or a blocked storage API in private browsing should not take down a React tree. But the fallback changes the meaning of the component: the same code that renders a saved list on one machine renders a session-only list on another, with no error to explain the difference. That is why the isPersistent flag is exposed rather than kept internal. A user interface that saves work has to be able to say so.

The failure modes that trigger it are the standard browser ones: storage disabled by the user, a full quota, and the privacy settings of some browsers. The library does not attempt to distinguish between them, and neither should the calling code unless it wants to, because the flag only reports the boolean.

For anything the user would be upset to lose, the right pattern is to treat isPersistent as false meaning the feature is degraded, not that it is working.

Server rendering brings a second default and a double render

The package supports server rendering, and the SSR surface is where the sharpest edge is. There are two default options instead of one. defaultValue is the normal starting value, and defaultServerValue is the value used while server rendering and hydrating. If defaultServerValue is not set, defaultValue is used for the server pass too. You set the second one only when the server value should differ from the client value.

There is a documented double render. If you hydrate a component, for example in a Next.js application, your component may render twice. The README attributes this to React rather than to the library, and names the cause: the useSyncExternalStore() hook. It states plainly that there is no workaround, and points to issue 56 for the discussion.

The package ships a helper for the case where you need to know which side you are on.

ts
function useIsServerRender() {
  return useSyncExternalStore(() => {
    return () => {}
  }, () => false, () => true)
}

The getSnapshot argument returns false on the client and true on the server, so the returned boolean tells you whether the current render is the server one. That is the hook the double render warning pushes you towards, and it is better than trying to suppress the render, which the documentation says cannot be done.

The serializer option is where Date and BigInt break

By default the hook serialises with JSON, and the documentation says so directly: JSON does not serialise Date, Regex, or BigInt data.

That limitation is easy to hit and hard to debug, because the value goes in cleanly and comes back as something else. A Date stored through this hook will not be a Date on the way out, and a BigInt cannot be serialised by JSON at all.

The serializer option exists for that reason. It takes an object with a stringify and a parse function, and the documentation points to superjson or any other JSON-compatible serialisation library for more advanced cases. The shape of the option means an alternative library can be dropped in without changing the hook, since it only has to provide those two functions.

One more option deserves a mention, and it is about type safety rather than storage. The key parameter is a plain string, the same string you pass to localStorage.setItem and getItem, and the documentation warns that it can collide with properties written by other parts of the codebase. There is no namespace helper and no prefixing, so the discipline has to come from the calling code.

Installing it, and the React 17 split

Installation is a single command for React 18 and above.

bash
npm install use-local-storage-state

React 17 and below are handled by a separate branch in the repository and a separate version tag on the registry.

bash
npm install use-local-storage-state@17

The documentation is split along the same line, with the older docs living on the react-17 branch. That is worth knowing before you search for an API that differs in the version you actually have installed.

A first use looks like this.

typescript
import useLocalStorageState from 'use-local-storage-state'

export default function Todos() {
    const [todos, setTodos] = useLocalStorageState('todos', {
        defaultValue: ['buy avocado', 'do 50 push-ups']
    })
}

The setter takes the same shapes a state setter takes, including a functional update, so the familiar pattern of setTodos([...todos, query]) works without a wrapper.

The working directory for the project itself is Bun rather than npm. The packageManager field is bun, the test script runs bun run build && bun run lint && vitest --run, the build is tsc, and a size script runs bun run build && size-limit, which is how the size claim in the README is kept honest.

When to write the three lines yourself instead

The honest counterpoint is that this hook is a small convenience over two browser APIs, and a team with strong opinions about state management may not want the dependency.

The sibling packages make the choice easier. use-storage-state supports localStorage, sessionStorage and any other Storage compatible API, and use-session-storage-state is a clone of this library for sessionStorage. If your component needs a value that disappears when the tab closes, that second package is the direct fit and this one is the wrong key.

A hand written useEffect is the third option, and it is not a bad one for a single value in a single component. What you lose is the parts this package handles that are easy to get wrong: the storage event subscription for other tabs, the fallback when the API throws, the explicit removal of the key, and the server rendering default that keeps hydration consistent.

What the package does not do is share state between components. Each hook instance owns its own key, and two components using the same key will overwrite each other rather than coordinate. If your requirement is a shared store, this hook is the wrong shape and a context or a state manager is the right one.

Editorial conclusion

Adopt use-local-storage-state when you want per component persistence with the useState shape and no third party state manager, and skip it when your data must be shared across components, must survive a cache quota failure, or needs a schema. Verify first that your React version is 18 or newer, since the React 17 path is a separate branch and a separate install tag, and namespace your storage keys, because the documentation warns that a key can collide with a property written elsewhere in the codebase or by an earlier version of the app.

Frequently asked questions

How do I install use-local-storage-state?

For React 18 and above run npm install use-local-storage-state. React 17 and below are served from a separate branch and a separate version tag, npm install use-local-storage-state@17, with their own documentation on the react-17 branch.

What does isPersistent tell me in use-local-storage-state?

It is a boolean that returns false when localStorage is throwing an error and the data is kept only in memory. The documentation suggests using it to notify the user that their changes are not being persisted.

Why does my component render twice when I hydrate with use-local-storage-state?

The README attributes it to React and the useSyncExternalStore() hook rather than to the library, and states there is no workaround. A useIsServerRender helper is provided so you can detect whether the current render is the server one.

Does use-local-storage-state sync between browser tabs?

Yes, by default it subscribes to the Window storage event, so updates propagate across tabs, windows and iframes. Setting the storageSync option to false turns that subscription off and stops cross tab synchronization.

Can I store a Date or a BigInt with use-local-storage-state?

Not with the default serializer, because JSON does not serialise Date, Regex or BigInt data. Pass a serializer option with stringify and parse functions, using a library such as superjson, to handle those types.

How do I clear a stored value in use-local-storage-state?

Call removeItem() from the third value returned by the hook. It calls localStorage.removeItem(key) and resets the hook to its default state, so the component returns to the condition it had when the hook was created.

Official sources

  1. astoilkov/use-local-storage-state on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/astoilkov-use-local-storage-state.svg)](https://hysenlabs.com/projects/astoilkov-use-local-storage-state)

Was this useful?