Building Practical Custom Hooks (useLocalStorage, useDebounce)0%
Composing Multiple Hooks and Custom Hook Testing

Building Practical Custom Hooks (useLocalStorage, useDebounce)

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Building Practical Custom Hooks (useLocalStorage, useDebounce)

1. The Power of Utility Custom Hooks

In real-world web engineering, certain stateful patterns recur across virtually every application:

  • Persisting state changes to browser storage automatically.
  • Debouncing rapid input typing before dispatching expensive search queries.

Let's build two of the most widely used enterprise utility hooks from scratch.

2. Hook 1: useLocalStorage

A hook that behaves exactly like useState, but automatically synchronizes its value with browser localStorage:

JavaScript
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
 
export function useLocalStorage(key, initialValue) {
// 1. Lazy initialization from localStorage
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
 
// 2. Synchronize to localStorage whenever storedValue changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(`Error saving localStorage key "${key}":`, error);
}
}, [key, storedValue]);
 
return [storedValue, setStoredValue];
}

Consumer Usage:

JSX
// Behaves just like useState, but survives page reloads!
const [theme, setTheme] = useLocalStorage('msk_theme', 'dark');
const [enrolledIds, setEnrolledIds] = useLocalStorage('msk_enrolled', []);

3. Hook 2: useDebounce

When a user types in a live search box, firing an API request on every keystroke floods your backend server with unnecessary network traffic.

Debouncing waits until the user has stopped typing for a specified delay (e.g. 500ms) before evaluating the value:

JavaScript
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';
 
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
 
useEffect(() => {
// Set a timer to update debouncedValue after the delay
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
 
// Cleanup: cancel the timer if 'value' or 'delay' changes (user kept typing!)
return () => {
clearTimeout(timer);
};
}, [value, delay]);
 
return debouncedValue;
}
JSX
import React, { useState, useEffect } from 'react';
import { useDebounce } from '../hooks/useDebounce';
 
export default function CourseSearch() {
const [searchTerm, setSearchTerm] = useState('');
// debouncedTerm updates ONLY after user stops typing for 500ms!
const debouncedTerm = useDebounce(searchTerm, 500);
 
useEffect(() => {
if (debouncedTerm) {
console.log(`Executing API search query for: ${debouncedTerm}`);
// fetchSearchResults(debouncedTerm);
}
}, [debouncedTerm]); // Only fires API query on debounced change!
 
return (
<div>
<input
type="text"
placeholder="Type to search curriculum..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<p>Instant Input: {searchTerm}</p>
<p>Debounced API Query: {debouncedTerm}</p>
</div>
);
}

Interactive Quiz

Practice Quiz

Question 1
In the useDebounce hook, how is the pending timer cancelled if the user presses another key before the delay expires?
Question 2
What is the primary benefit of debouncing a search input before calling an API?
Question 3
Why does useLocalStorage use JSON.stringify and JSON.parse?
Question 4
Why is useLocalStorage's initial read wrapped in a try...catch block?
Question 5
What is returned by const [val, setVal] = useLocalStorage('key', 'default')?
Next Lesson

Composing Multiple Hooks and Custom Hook Testing

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext