Welcome back!
In our last lesson, we explored how to reuse a custom hook across multiple components. You saw firsthand that custom hooks share logic, but each component that uses the hook gets its own independent state. This is a cornerstone of writing clean and scalable React applications.
Today, we'll tackle a very common challenge in web development: state that disappears when the user refreshes the page. We'll build a powerful and practical custom hook, useLocalStorage, that solves this by persisting state in the browser.
By the end of this lesson, you will be able to build a useLocalStorage custom hook to persist state in the browser. This hook will mimic the familiar useState API but with the added superpower of "stickiness."
The Problem: Volatile State
So far, all the state we've managed with useState or our useCounter hook lives only in the computer's memory. When you refresh the page or close the tab, that memory is wiped clean, and your application resets to its initial state.
Imagine a user toggling your app to "dark mode" or filling out a long form. It would be frustrating if their preferences or progress vanished on a simple page refresh. We need a way to save this data somewhere more permanent.
The Solution: Browser localStorage
Fortunately, modern browsers provide a simple key-value storage mechanism called localStorage. It allows us to save small amounts of data (as strings) that persist even after the browser is closed and reopened.
Here's what you need to know about it:
localStorage.setItem(key, value): Saves avalueassociated with akey.localStorage.getItem(key): Retrieves the value for a givenkey.- It only stores strings: If you want to store a number, array, or object, you must first convert it to a JSON string using
JSON.stringify(). When you retrieve it, you'll need to parse it back withJSON.parse().
Our goal is to create a hook that handles all this logic for us, giving us a simple useState-like interface.
Designing Our useLocalStorage Hook
Before writing code, let's think about how we want to use our new hook. It should feel almost identical to useState, but with one extra piece of information: the key to use for localStorage.
// Instead of this:
// const [name, setName] = useState('Guest');
// We want this:
const [name, setName] = useLocalStorage('username', 'Guest');
// 'username' is the key for localStorage
// 'Guest' is the initial value if nothing is stored yet
This design tells us our hook needs two arguments (key, initialValue) and must return an array with two elements (value, setValue), just like useState.
Building the Hook: Step-by-Step
A custom hook is just a JavaScript function that uses other hooks. Our useLocalStorage hook will be built by combining useState and useEffect.
The process breaks down into two main parts:
- Initializing the state: When the component first mounts, read the value from
localStorage. If nothing is there, use the provided initial value. - Persisting the state: Whenever the state changes, save the new value back to
localStorage.
The video below from Web Dev Simplified provides an excellent, concise walkthrough of this entire process.
Learn Custom Hooks In 10 Minutes
This video will guide you through creating the useLocalStorage hook from scratch. It clearly explains how to combine useState and useEffect to achieve our goal.
Watch from the beginning (00:49) to 07:28. The video covers: Setting up the custom hook file and function signature. Using useState to manage the internal state. Lazily initializing the state from localStorage. Using useEffect to write back to localStorage whenever the state changes.
Let's break down the key concepts from that video.
1. Initializing State with Lazy Initialization
When initializing our state, we need to read from localStorage. This is an I/O operation and can be slow compared to simply reading a variable. We only want to do this once when the component first renders, not on every re-render.
useState supports a "lazy initialization" pattern for this. Instead of passing a value directly, you can pass a function. React will only execute this function on the initial render.
const [value, setValue] = useState(() => {
// This function only runs once!
const savedValue = localStorage.getItem(key);
if (savedValue !== null) {
return JSON.parse(savedValue);
}
return initialValue;
});
This is a crucial performance optimization. Josh W. Comeau's article on this topic provides a great explanation of this pattern.
Persisting React State in localStorage
To solidify your understanding of this important pattern, let's read a clear explanation from Josh W. Comeau's blog.
Please read the section titled 'How it works', focusing on the subsection 'Lazy initialization'. It provides a concise breakdown of why we pass a function to useState.
2. Persisting State with useEffect
Now we need to handle updates. Whenever our state value changes, we need to perform a "side effect": writing that new value to localStorage. The useEffect hook is the perfect tool for this.
We'll set up an effect that runs every time the value or key changes.
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
By including key and value in the dependency array, we ensure that localStorage is updated whenever either of them changes.
Your Turn: Create and Use useLocalStorage
Now it's time to put it all together.
1. Project Structure
It's a good convention to keep custom hooks in a dedicated hooks directory inside src.

Create a new file: src/hooks/useLocalStorage.js.
2. Implement the Hook
Based on the video and explanations, write the code for the useLocalStorage hook in your new file. The final code should look similar to this:
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
// 1. Get initial value from localStorage or use initialValue
const [value, setValue] = useState(() => {
try {
const savedValue = window.localStorage.getItem(key);
if (savedValue !== null) {
return JSON.parse(savedValue);
}
return initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// 2. Use useEffect to update localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.log(error);
}
}, [key, value]);
return [value, setValue];
}
Note: We've wrapped the localStorage access in try...catch blocks. This is a robust way to handle cases where localStorage might be disabled by the user's browser settings.
3. Use Your New Hook
Now, let's use it in your App.js component. We'll create a simple text input that remembers its value after a refresh.
// src/App.js
import { useLocalStorage } from './hooks/useLocalStorage';
function App() {
const [name, setName] = useLocalStorage('username', ''); // Use our hook!
return (
<div>
<h1>Persistent Input</h1>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ padding: '8px', fontSize: '1rem' }}
/>
<p>Hello, {name || 'stranger'}!</p>
</div>
);
}
export default App;
Instructions:
- Run your application.
- Type your name into the input field. You should see the greeting message update.
- Refresh the page.
- Observe that your name is still in the input field!
- You can also open your browser's developer tools, go to the "Application" tab (in Chrome/Edge) or "Storage" tab (in Firefox), select "Local Storage," and see the key
usernamewith the value you typed.
The beauty of this is how little the component code had to change. You swapped useState for useLocalStorage, and it just worked. This is the power of creating good abstractions with custom hooks.
For another excellent perspective on building this hook, you can watch Kent C. Dodds' video. He builds the logic inside the component first and then extracts it, which really highlights the refactoring process.
Create a custom useLocalStorage React hook
This video from Kent C. Dodds offers an alternative walkthrough. He demonstrates the power of this pattern by first implementing the logic directly in a component and then abstracting it into a reusable hook.
If you'd like another viewpoint, watch from 04:01 to 10:57. This covers building the useEffect and useState logic inline and then extracting it into the useLocalStorageState custom hook.
Conclusion
Excellent work! You have successfully built one of the most common and useful custom hooks. You've combined useState and useEffect to create a new, powerful piece of reusable logic that solves a real-world problem.
Key Takeaways:
localStorageis a browser feature for persisting small amounts of string data across sessions.- Custom hooks can abstract away complex logic, like interacting with browser APIs.
- The
useLocalStoragehook combinesuseStatefor state management anduseEffectto sync that state withlocalStorage. - Using the lazy initializer function
useState(() => { ... })is a key performance optimization for reading initial state from slow sources likelocalStorage. - Always use
JSON.stringifywhen saving to andJSON.parsewhen reading fromlocalStorageto handle non-string data types.
Next Up:
You've now mastered creating and reusing custom hooks to share logic. But what about sharing state? In our next module, we'll address the problem of "prop drilling," where you have to pass props through many layers of components. You'll learn how to use React's Context API to share state efficiently across your entire application.