Managing persistent data in your React application is a common requirement, and browser localStorage
can help with this. In this article, we’ll break down how to create a custom React hook, useLocalStorage
, for seamless local storage integration. This hook not only allows for saving, retrieving, and deleting data from localStorage
, but it also provides an intuitive interface for state management.
1. Utilities for Local Storage
Before we dive into the hook, let’s create a set of utility functions to interact with localStorage
. These utilities will handle setting, retrieving, and removing items while managing potential errors.
setItem
: Safely Save Data to Local Storage
The setItem function takes a key
and a value
and stores the serialized value in localStorage
.
export function setItem(key: string, value: unknown) {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error(err);
}
}
-
What it does:
- Serializes the value using
JSON.stringify
. - Saves the data to
localStorage
under the provided key.
- Serializes the value using
- Error Handling: If there’s an issue (e.g., storage quota exceeded), it logs the error.
getItem:
Retrieve and Parse Data
The getItem
function retrieves data by a key and parses it back to its original format.
export function getItem<T>(key: string): T | undefined {
try {
const data = window.localStorage.getItem(key);
return data ? (JSON.parse(data) as T) : undefined;
} catch (err) {
console.error(err);
}
}
-
What it does:
- Fetches data using the provided key.
- Parses the serialized value back to its original type.
- Returns
undefined
if the key doesn’t exist.
- Type-Safety: Uses TypeScript’s generic T to ensure type consistency.
removeItem:
Remove Data by Key
The removeItem
function deletes the stored value associated with a key.
export function removeItem(key: string) {
try {
window.localStorage.removeItem(key);
} catch (err) {
console.error(err);
}
}
-
What is does:
- Calls
localStorage.removeItem
to delete the key-value pair. - Catches and logs any errors that occur.
With these utility functions in place, we now have robust tools to interact with
localStorage
. Next, let’s integrate them into our custom hook.
- Calls
The useLocalStorage
Hook
React hooks provide a clean way to manage state and side effects. Let’s use them to create a useLocalStorage
hook that combines stateful logic with our localStorage utilities.
Hook Initialization
Here’s the basic structure of the hook:
import { useState } from "react";
import { getItem, setItem, removeItem } from "@/utils/localStorage";
export default function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState(() => {
const data = getItem(key);
return (data || initialValue) as T;
});
// ...additional logic
}
-
Parameters:
-
key
: ThelocalStorage
key to interact with. -
initialValue
: The default value to use if no value exists inlocalStorage
.
-
-
State Initialization:
- The hook initializes its state (
value
) by callinggetItem
to check for existing data inlocalStorage
. - If no data exists, it uses the provided
initialValue
.
- The hook initializes its state (
Dispatching State Changes
The handleDispatch
function manages updates to both the local state and localStorage
.
type DispatchAction<T> = T | ((prevState: T) => T);
function handleDispatch(action: DispatchAction<T>) {
if (typeof action === "function") {
setValue((prevState) => {
const newValue = (action as (prevState: T) => T)(prevState);
setItem(key, newValue);
return newValue;
});
} else {
setValue(action);
setItem(key, action);
}
}
-
How It Works:
- If
action
is a function, it treats it as a reducer-like updater, applying it to the current state (prevState
). - Otherwise, it assumes
action
is the new value.
- If
-
Local Storage Sync:
- After updating the state, it stores the new value in
localStorage
.
- After updating the state, it stores the new value in
Clearing the State
Sometimes, you may want to reset the state and remove the corresponding localStorage
data. This is handled by the clearState
function.
function clearState() {
setValue(undefined as T);
removeItem(key);
}
-
What it does:
- Resets the state to
undefined
. - Removes the associated
localStorage
key-value pair.
- Resets the state to
Returning the Hook’s API
Finally, the hook returns an array of three elements:
return [value, handleDispatch, clearState] as const;
-
API:
- 1.
value
: The current state. - 2.
handleDispatch
: Function to update the state. - 3.
clearState
: Function to reset the state andlocalStorage
.
- 1.
3. Using the useLocalStorage
Hook
Here’s an example of how you can use this hook in a React component:
import useLocalStorage from "@/hooks/useLocalStorage";
function Counter() {
const [count, setCount, clearCount] = useLocalStorage<number>("counter", 0);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
<button onClick={clearCount}>Reset</button>
</div>
);
}
-
Key Features:
- The
count
value is persisted across page reloads usinglocalStorage
. - Updates to the state (
setCount
) automatically sync withlocalStorage
. - The
clearCount
function resets the counter and removes it fromlocalStorage
.
- The
Full code
localStorage.ts
code:
export function setItem(key: string, value: unknown) {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error(err);
}
}
export function getItem<T>(key: string): T | undefined {
try {
const data = window.localStorage.getItem(key);
return data ? (JSON.parse(data) as T) : undefined;
} catch (err) {
console.error(err);
}
}
export function removeItem(key: string) {
try {
window.localStorage.removeItem(key);
} catch (err) {
console.error(err);
}
}
useLocalStorage.ts
code:
import { getItem, removeItem, setItem } from "@/utils/localStorage";
import { useState } from "react";
type DispatchAction<T> = T | ((prevState: T) => T);
export default function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState(() => {
const data = getItem(key);
return (data || initialValue) as T;
});
function handleDispatch(action: DispatchAction<T>) {
if (typeof action === "function") {
setValue((prevState) => {
const newValue = (action as (prevState: T) => T)(prevState);
setItem(key, newValue);
return newValue;
});
} else {
setValue(action);
setItem(key, action);
}
}
function clearState() {
setValue(undefined as T);
removeItem(key);
}
return [value, handleDispatch, clearState] as const;
}
Conclusion
The useLocalStorage
hook is a powerful and reusable abstraction for managing state that persists across page reloads. It’s type-safe, handles errors gracefully, and offers an intuitive interface for developers. With this hook, you can easily integrate localStorage
into your React applications and keep your codebase clean and maintainable.
Have you tried building your own custom hooks? Share your experiences in the comments below!
Top comments (2)
useLocalStorage is a basic react hook that everyone that uses react has to know, use or build someday. This is a simple yet great way to build one. The only thing that I would change is that on the
localStorage.ts
functions, I wouldn't console.log the error, put pass the error to the next function.On my latest project, I started passing function results as
[error, value]
so the person that is using the code can decide how he wants to deal with the error. This is specially good with codes like this, that will be used everywhere in an app. In cases the error is just a nunsense, the dev can just ignore it. In cases it can break the app, he sends the log somewhere.Where I work we have a version of this hook though we've gone about it in a slightly different way. Rather than attaching a state to specific entry we have a set of function that allow you (as the developer) to interact with localstorage directly. We even have event listeners so you can edit an entry else where in the app (or in a different tab) and you can catch that update and do whatever you need to.
You can check it out here if you like 😊