Hi everyone! I'm Gaurav, and I am incredibly excited to share my very first article with the DEV Community! As a developer, I love building open-source tools that solve everyday workflow headaches and make life a bit easier for the community. I wrote this article to share what I learned while building my latest package, and I hope it helps you level up your React storage management.
When we build React apps, we often need to save small values in the browser.
For example:
- selected theme
- user settings
- form draft
- search filters
- temporary session value
- cached API response
The browser already gives us localStorage, but using it directly in React can become repetitive.
We usually need to:
- read the value from
localStorage - parse the saved JSON
- handle missing or broken data
- update React state
- write the new value back to
localStorage - remove the value when needed
- sync the value between tabs
- expire old values after some time
This is why I built @gks101/localyx.
It is a small React hook that helps you use localStorage like normal React state.
Main Features
- simple
useState-like API - saves state in
localStorage - Same Tab and Cross Tab Sync
- TTL expiry support
- absolute and sliding expiry strategies
- namespace support
- custom serializer support
- custom encrypt and decrypt support
- helper function to check remaining TTL
Working demo:
https://localyx.vercel.app/
GitHub repo:
https://github.com/gaurav101/localyx
NPM package:
npm install @gks101/localyx
What Problem Does It Solve?
React state is simple:
const [value, setValue] = useState("hello");
But normal React state is lost when the page is refreshed.
localStorage can keep data after refresh, but it does not work like React state by default.
So I wanted a hook that feels like useState, but also saves the value in localStorage.
That is the main idea of useLocalStorageState.
Basic Usage
First install the package:
npm install @gks101/localyx
Then import the hook:
import { useLocalStorageState } from "@gks101/localyx";
Now use it inside a component:
import { useLocalStorageState } from "@gks101/localyx";
function ThemeToggle() {
const [theme, setTheme, clearTheme] = useLocalStorageState("theme", "light");
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
<button onClick={clearTheme}>
Reset Theme
</button>
</div>
);
}
This looks very close to useState.
The hook returns three values:
const [state, setState, removeValue] = useLocalStorageState(key, initialValue);
Step 1: The Key
The first value is the localStorage key.
useLocalStorageState("theme", "light");
Here, "theme" is the key.
The hook will save the value in localStorage using this key.
Step 2: The Initial Value
The second value is the default value.
useLocalStorageState("theme", "light");
Here, "light" is the initial value.
The hook will use this value when:
- nothing is saved in
localStorage - the saved data is invalid
- the saved data has expired
- you remove the value
Step 3: Reading the State
The first returned value is the current state.
const [theme] = useLocalStorageState("theme", "light");
You can show it in your UI:
<p>Current theme: {theme}</p>
When the page refreshes, the hook reads the saved value from localStorage.
Step 4: Updating the State
The second returned value is the setter function.
const [theme, setTheme] = useLocalStorageState("theme", "light");
You can use it like setState:
setTheme("dark");
You can also use the previous value:
setTheme((oldTheme) => oldTheme === "light" ? "dark" : "light");
When you call setTheme, the hook updates React state and also saves the new value in localStorage.
Step 5: Removing the Value
The third returned value removes the saved data.
const [theme, setTheme, clearTheme] = useLocalStorageState("theme", "light");
Use it like this:
clearTheme();
This removes the value from localStorage and sets the state back to the initial value.
TTL: Expire Data After Some Time
Sometimes we do not want to keep data forever.
For example, a session value or cache value should expire after some time.
TTL means "Time To Live".
You can pass ttl in milliseconds.
import { useLocalStorageState } from "@gks101/localyx";
function SessionExample() {
const [session, setSession, clearSession] = useLocalStorageState<string | null>(
"session",
null,
{
ttl: 30 * 60 * 1000,
}
);
return (
<div>
<p>{session ?? "No active session"}</p>
<button onClick={() => setSession("my-session-token")}>
Login
</button>
<button onClick={clearSession}>
Logout
</button>
</div>
);
}
In this example, the session expires after 30 minutes.
After expiry, the hook removes the old value and returns the initial value.
Absolute TTL
By default, TTL uses the absolute strategy.
This means the timer starts when the value is saved.
Example:
const [token, setToken] = useLocalStorageState("token", null, {
ttl: 10 * 60 * 1000,
ttlStrategy: "absolute",
});
If the value is saved at 10:00, it will expire at 10:10.
Even if the user keeps using the app, the value still expires at that time.
This is useful for things like:
- login session expiry
- one-time temporary values
- short-lived cache
Sliding TTL
The hook also supports sliding TTL.
const [searchCache, setSearchCache] = useLocalStorageState("search_cache", "", {
ttl: 5 * 60 * 1000,
ttlStrategy: "sliding",
});
Sliding TTL refreshes the timer when the value is read or synced.
This means active data can stay alive while the user is still using it.
This is useful for:
- active session-like data
- recently used filters
- temporary UI cache
Run Code When a Value Expires
You can pass onExpire.
This function runs when the hook finds that the value has expired.
const [token, setToken] = useLocalStorageState("token", null, {
ttl: 10 * 60 * 1000,
onExpire: ({ key, value }) => {
console.log("Expired key:", key);
console.log("Expired value:", value);
},
});
This is useful when you want to:
- show a message
- logout the user
- clear related data
- send an analytics event
Namespaces
Sometimes many features use localStorage.
To avoid key conflicts, you can use a namespace.
const [profile, setProfile] = useLocalStorageState("profile", null, {
namespace: "my-app",
});
This stores the value using this key:
my-app:profile
Namespaces are useful when:
- you have many stored values
- you are building a large app
- you want to separate app versions
- you want cleaner
localStoragekeys
Same Tab and Cross Tab Sync
If you open the same app in two browser tabs, the hook can keep values in sync.
For example:
- Open the app in Tab 1.
- Open the app in Tab 2.
- Change the theme in Tab 1.
- Tab 2 gets the new value too.
The hook also handles same-tab sync.
This is useful when two components use the same storage key on the same page.
Get Remaining TTL
The package also exports getRemainingTtl.
This helper tells you how much time is left before a saved value expires.
import { getRemainingTtl } from "@gks101/localyx";
const remainingMs = getRemainingTtl("session", 30 * 60 * 1000);
console.log(remainingMs);
It returns:
- a number if time is remaining
-
0if the value is already expired but not cleaned yet -
nullif the key does not exist or the value has no TTL data
You can also use it with a namespace:
const remainingMs = getRemainingTtl("session", 30 * 60 * 1000, {
namespace: "my-app",
});
Custom Serialization
By default, the hook uses JSON.
For most React apps, that is enough.
But you can provide your own serializer if you need custom behavior.
const [value, setValue] = useLocalStorageState("custom", "hello", {
serializer: {
stringify: (value) => JSON.stringify(value),
parse: (value) => JSON.parse(value),
},
});
This is useful when you want more control over how data is saved and loaded.
Encoding and Encryption
By default, the hook stores data with Base64 encoding.
Base64 is only obfuscation. It is not real security.
If you need real security, pass your own encrypt and decrypt functions.
const encrypt = (raw: string) => {
return btoa(raw);
};
const decrypt = (raw: string) => {
return atob(raw);
};
const [secret, setSecret] = useLocalStorageState("secret", "hello", {
encrypt,
decrypt,
});
If you want to store plain JSON without Base64 encoding, pass null:
const [value, setValue] = useLocalStorageState("plain", "hello", {
encrypt: null,
decrypt: null,
});
Important note: do not store highly sensitive data in localStorage unless you fully understand the security risks.
SSR Safe
The hook has guards for browser-only APIs.
This means it can be used in apps that render on the server, like Next.js.
It will not directly access window or localStorage when they are not available.
Where Can You Use This Hook?
You can use this hook in many React projects.
Good use cases:
- dark mode or theme setting
- language preference
- sidebar open or closed state
- table filters
- search filters
- sort order
- form draft
- shopping cart for small projects
- temporary session value
- API cache with expiry
- feature flag cache
- onboarding step progress
- recently viewed items
Avoid using it for:
- passwords
- private tokens without a security plan
- very large data
- data that must always be correct on the server
You Can Also Copy the Hook Into Your Project
You do not have to use the npm package if you do not want another dependency.
You can also copy the hook file from the GitHub repo and use it directly in your project.
Repo link:
https://github.com/gaurav101/localyx
This can be useful when:
- you want full control over the code
- you want to change the hook for your app
- you are learning how the hook works
- you want to avoid adding a package
Full API
Main hook:
useLocalStorageState<T>(key, initialValue, options?)
It returns:
[state, setState, removeValue]
Options:
{
ttl?: number;
ttlStrategy?: "absolute" | "sliding";
namespace?: string;
onExpire?: (ctx: { key: string; value: T }) => void;
now?: () => number;
encrypt?: ((raw: string) => string) | null;
decrypt?: ((raw: string) => string) | null;
serializer?: {
stringify: (value: T) => string;
parse: (value: string) => T;
};
}
Helper:
getRemainingTtl(key, ttl, options?)
Final Thoughts
I built @gks101/localyx because I wanted a simple way to use localStorage in React without writing the same logic again and again.
It gives you a useState-like API, but also adds useful features like expiry, sync, namespaces, and custom serialization.
If you are a React beginner, you can start with the basic example first.
Later, when you need expiry or sync, the hook already supports it.
Package:
npm install @gks101/localyx
GitHub:
https://github.com/gaurav101/localyx
I would absolutely love to hear your thoughts, feedback, or any questions you have in the comments below—let's connect!
Top comments (0)