Custom Hooks
Create reusable custom hooks for shared logic patterns
Custom Hooks in React
Master the art of building reusable logic with custom React hooks through free flashcards and spaced repetition practice. This lesson covers hook creation patterns, state management encapsulation, and best practices for composing custom hooksβessential skills for writing clean, maintainable React applications.
Welcome to Custom Hooks π£
Custom hooks are one of React's most powerful features, allowing you to extract component logic into reusable functions. They're the secret weapon for eliminating code duplication, organizing complex logic, and creating a library of shareable utilities that work seamlessly across your entire application.
Think of custom hooks as your personal toolbox π§°. Just as a carpenter creates specialized tools for repeated tasks, you'll create custom hooks for common patterns in your React applications. Once you master this skill, you'll write less code, maintain cleaner components, and ship features faster.
Core Concepts: Understanding Custom Hooks π‘
What Makes a Hook "Custom"?
A custom hook is simply a JavaScript function whose name starts with "use" and that may call other hooks. That's it! The "use" prefix is a convention that tells React (and developers) that this function follows the Rules of Hooks.
βββββββββββββββββββββββββββββββββββββββββββ β HOOK HIERARCHY β βββββββββββββββββββββββββββββββββββββββββββ€ β β β Built-in Hooks (useState, useEffect) β β β β β Custom Hooks (useForm, useFetch) β β β β β Components (LoginForm, UserProfile) β β β βββββββββββββββββββββββββββββββββββββββββββ
Here's the simplest possible custom hook:
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
This hook encapsulates the logic for setting the browser's document title. Any component can now use it:
function ProfilePage() {
useDocumentTitle('User Profile');
return <div>Profile content...</div>;
}
Why Create Custom Hooks? π―
1. Code Reusability: Instead of copying the same logic across multiple components, write it once in a custom hook.
2. Separation of Concerns: Keep your components focused on rendering UI while hooks handle complex logic.
3. Testability: Custom hooks can be tested independently from components.
4. Composition: Hooks can call other hooks, allowing you to build complex behavior from simple pieces.
The Anatomy of a Custom Hook π¬
Let's examine a real-world custom hook that manages form state:
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => {
setValue(e.target.value);
};
const reset = () => {
setValue(initialValue);
};
return {
value,
onChange: handleChange,
reset
};
}
This hook:
- β Starts with "use"
- β Uses other hooks (useState)
- β Returns values that components can use
- β Encapsulates related logic (state + handlers)
Rules of Custom Hooks β οΈ
Custom hooks must follow the same rules as built-in hooks:
| Rule | Why It Matters |
|---|---|
| Only call at top level | React relies on hook call order to track state |
| Only call from React functions | Hooks need React's rendering context |
| Name starts with "use" | Enables linting and signals hook behavior |
β Wrong:
function MyComponent() {
if (condition) {
const data = useFetch(url); // Conditional hook call!
}
}
β Right:
function MyComponent() {
const data = useFetch(condition ? url : null); // Hook always called
}
Example 1: useToggle - Managing Boolean State π
One of the most common patterns is toggling boolean state (modals, menus, dark mode, etc.). Let's create a reusable hook:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []);
const setTrue = useCallback(() => {
setValue(true);
}, []);
const setFalse = useCallback(() => {
setValue(false);
}, []);
return [value, { toggle, setTrue, setFalse }];
}
Key features:
- Returns array like useState (destructuring syntax)
- Includes helper functions beyond just setState
- Uses useCallback to memoize functions
Usage in components:
function Modal() {
const [isOpen, { toggle, setTrue, setFalse }] = useToggle();
return (
<>
<button onClick={setTrue}>Open Modal</button>
{isOpen && (
<div className="modal">
<h2>Modal Content</h2>
<button onClick={setFalse}>Close</button>
</div>
)}
</>
);
}
function Sidebar() {
const [isExpanded, { toggle }] = useToggle(true);
return (
<aside className={isExpanded ? 'expanded' : 'collapsed'}>
<button onClick={toggle}>Toggle Sidebar</button>
{/* sidebar content */}
</aside>
);
}
π‘ Why this works: The same toggle logic is now reusable across any component. Notice how each component gets its own independent stateβReact creates a new instance of the hook for each component that uses it.
Example 2: useFetch - Handling Async Data π
Fetching data is perhaps the most common side effect in React apps. Let's build a robust custom hook:
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Handle empty URL
if (!url) {
setLoading(false);
return;
}
let isCancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (!isCancelled) {
setData(json);
setLoading(false);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
// Cleanup function
return () => {
isCancelled = true;
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
What's happening here:
- Three pieces of state: data, loading, and error
- Effect dependency: Re-fetches when URL or options change
- Cleanup mechanism: Prevents state updates after unmount
- Error handling: Catches network and HTTP errors
Using the hook:
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(
`https://api.example.com/users/${userId}`
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
π§ Try this: Add a refetch function to the return value that manually triggers a new fetch:
const refetch = useCallback(() => {
setLoading(true);
// trigger fetch again
}, [url]);
return { data, loading, error, refetch };
Example 3: useLocalStorage - Persisting State πΎ
Synchronizing state with localStorage is another common pattern:
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Return wrapped version of useState's setter
const setValue = useCallback((value) => {
try {
// Allow value to be a function like useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
// Remove item from localStorage
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(error);
}
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
Key techniques:
- Lazy initialization: useState accepts a function to compute initial state
- Try-catch blocks: Handles quota exceeded and parsing errors
- Function updaters: Supports
setValue(prev => prev + 1)syntax - Multiple return values: Provides remove functionality
Practical usage:
function ThemeToggle() {
const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
function ShoppingCart() {
const [cart, setCart] = useLocalStorage('cart', []);
const addItem = (item) => {
setCart(prev => [...prev, item]);
};
const clearCart = () => {
setCart([]);
};
return (
<div>
<div>Items in cart: {cart.length}</div>
{/* cart UI */}
</div>
);
}
π§ Memory device: Think of useLocalStorage as "useState with a backup disk." Every state change gets automatically saved and restored.
Example 4: useDebounce - Optimizing Performance β‘
Debouncing is crucial for search inputs, resize handlers, and any high-frequency events:
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set up the timeout
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: cancel timeout if value changes before delay
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
How it works:
USER TYPES: "r" β "re" β "rea" β "reac" β "react"
β β β β β
TIMEOUTS: 500ms 500ms 500ms 500ms 500ms
(cancelledΓ4) β fires
β
API CALL: "react"
Only the final value triggers the API call, saving unnecessary requests!
Real-world application:
function SearchUsers() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { data: users, loading } = useFetch(
debouncedSearchTerm
? `https://api.example.com/users?search=${debouncedSearchTerm}`
: null
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search users..."
/>
{loading && <div>Searching...</div>}
{users && (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
π‘ Pro tip: You can also create useThrottle with a similar pattern, but it fires at regular intervals instead of waiting for silence.
Composing Custom Hooks π
The real power emerges when you combine custom hooks. Here's a hook that uses multiple other hooks:
function useApiData(url) {
const { data, loading, error, refetch } = useFetch(url);
const [isStale, setIsStale] = useState(false);
// Mark data as stale after 5 minutes
useEffect(() => {
if (!data) return;
const timer = setTimeout(() => {
setIsStale(true);
}, 5 * 60 * 1000);
return () => clearTimeout(timer);
}, [data]);
// Auto-refresh if stale and window gains focus
useEffect(() => {
const handleFocus = () => {
if (isStale) {
refetch();
setIsStale(false);
}
};
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [isStale, refetch]);
return { data, loading, error, isStale, refetch };
}
This hook composes:
useFetchfor data fetchinguseStatefor staleness trackinguseEffectfor timers and event listeners
Common Mistakes β οΈ
1. Breaking the Rules of Hooks
β Calling hooks conditionally:
function useBadExample(shouldFetch) {
if (shouldFetch) {
const data = useFetch(url); // WRONG!
}
}
β Pass conditions to the hook:
function useGoodExample(shouldFetch) {
const data = useFetch(shouldFetch ? url : null); // RIGHT!
}
2. Forgetting Dependencies
β Missing dependencies:
function useBadEffect(id) {
useEffect(() => {
fetchData(id); // Uses 'id' but not in deps!
}, []); // WRONG!
}
β Include all dependencies:
function useGoodEffect(id) {
useEffect(() => {
fetchData(id);
}, [id]); // RIGHT!
}
3. Not Cleaning Up Effects
β Memory leaks:
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay);
// No cleanup! WRONG!
}, [callback, delay]);
}
β Always clean up:
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id); // RIGHT!
}, [callback, delay]);
}
4. Overcomplicating Return Values
β Confusing API:
function useBadApi() {
return {
theDataValue: data,
isCurrentlyLoading: loading,
theErrorIfAny: error
};
}
β Keep it simple:
function useGoodApi() {
return { data, loading, error };
}
5. Recreating Functions on Every Render
β New function every render:
function useForm() {
const [value, setValue] = useState('');
return {
value,
onChange: (e) => setValue(e.target.value) // New function each time!
};
}
β Memoize with useCallback:
function useForm() {
const [value, setValue] = useState('');
const onChange = useCallback((e) => {
setValue(e.target.value);
}, []);
return { value, onChange };
}
Advanced Patterns π
Hook Factories
Create hooks that generate other hooks:
function createUseStorage(storageType) {
return function useStorage(key, initialValue) {
const storage = window[storageType];
const [value, setValue] = useState(() => {
try {
const item = storage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const updateValue = useCallback((newValue) => {
setValue(newValue);
storage.setItem(key, JSON.stringify(newValue));
}, [key]);
return [value, updateValue];
};
}
// Now create specialized hooks
const useLocalStorage = createUseStorage('localStorage');
const useSessionStorage = createUseStorage('sessionStorage');
Hooks with TypeScript π
Custom hooks benefit greatly from TypeScript:
function useFetch<T>(url: string | null): {
data: T | null;
loading: boolean;
error: string | null;
} {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// ... implementation
return { data, loading, error };
}
// Usage with type inference
interface User {
id: number;
name: string;
}
const { data: user } = useFetch<User>('/api/user');
// 'user' is typed as User | null
Key Takeaways π―
π Custom Hooks Quick Reference
| What they are | Reusable functions that use React hooks |
| Naming convention | Must start with "use" |
| Key benefit | Extract and share component logic |
| Can call | Other hooks (built-in or custom) |
| Can return | Any value (state, functions, objects, arrays) |
| Must follow | Rules of Hooks (top-level, React functions only) |
| Best for | Data fetching, subscriptions, timers, form handling |
| Testing | Use @testing-library/react-hooks |
Remember these principles:
- π― Single Responsibility: Each hook should do one thing well
- π Composition over Complexity: Build complex hooks from simple ones
- π Clear APIs: Return values should be intuitive and well-documented
- π§Ή Clean Up: Always return cleanup functions from effects
- β‘ Performance: Memoize callbacks and values when needed
- π§ͺ Testability: Design hooks to be testable independently
π Further Study
- React Official Docs - Building Your Own Hooks: https://react.dev/learn/reusing-logic-with-custom-hooks
- useHooks - Collection of React Hook Recipes: https://usehooks.com/
- React Hooks Testing Library: https://react-hooks-testing-library.com/
π You now have the foundation to create custom hooks! Start by identifying repeated logic in your components, extract it into a hook, and watch your codebase become cleaner and more maintainable. The best way to learn is by buildingβtry creating hooks for your own common patterns.