Building Your First React Custom Hook
- Published on
- 5 min read
- Authors

- Name
- Robin te Hofstee
- @Robin_teHofstee
React hooks changed the way we write components, but did you know you can create your own? Custom hooks are one of React's most powerful features, allowing you to extract and reuse logic across your application.
What is a Custom Hook?
A custom hook is simply a JavaScript function that uses other React hooks. The key rule: it must start with "use" (like useState or useEffect).
Think of custom hooks as recipes. Instead of copying the same cooking steps into every meal, you write the recipe once and reuse it whenever needed.
Your First Custom Hook: useToggle
Let's start simple. How many times have you written this pattern?
function Modal() {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const toggle = () => setIsOpen(!isOpen);
return (
// Modal code using isOpen, open, close, toggle
);
}
This toggle pattern appears everywhere: modals, dropdowns, sidebars, accordions. Let's extract it into a custom hook:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
Now you can use it anywhere:
function Modal() {
const [isOpen, { toggle, setTrue, setFalse }] = useToggle(false);
return (
<>
<button onClick={setTrue}>Open Modal</button>
{isOpen && (
<div className="modal">
<h2>My Modal</h2>
<button onClick={setFalse}>Close</button>
</div>
)}
</>
);
}
Much cleaner! And you can reuse it in every component that needs toggle behavior.
A Practical Hook: useFetch
Fetching data is something you do constantly. Here's a custom hook that handles loading states, errors, and data:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
Now fetching data becomes incredibly simple:
function BlogPosts() {
const { data, loading, error } = useFetch('/api/posts');
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{data.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.summary}</p>
</article>
))}
</div>
);
}
Advanced Hook: useLocalStorage
This hook syncs state with localStorage, so your data persists across page refreshes:
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use default
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Update localStorage whenever value changes
const setValue = (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);
}
};
return [storedValue, setValue];
}
Use it exactly like useState, but with persistence:
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
The theme persists even after closing the browser!
Rules for Custom Hooks
- Always start with "use" - This lets React know it's a hook
- Only call hooks at the top level - No hooks inside loops, conditions, or nested functions
- Only call hooks from React functions - Components or other custom hooks
- Keep them focused - Each hook should do one thing well
When to Create a Custom Hook
Create a custom hook when you:
- Find yourself copying the same logic across components
- Have complex state logic that's hard to understand
- Want to separate concerns (like data fetching from UI)
- Need to share stateful logic without render props or HOCs
Real-World Example: useWindowSize
Here's a hook I use constantly for responsive designs:
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? (
<MobileMenu />
) : (
<DesktopMenu />
)}
</div>
);
}
Tips for Better Custom Hooks
Return an object for many values:
// Good for multiple related values
return { data, loading, error, refetch };
Return an array for simple cases:
// Good for 1-2 values used together
return [value, setValue];
Add cleanup when needed:
useEffect(() => {
const subscription = subscribeToSomething();
return () => subscription.unsubscribe(); // Cleanup!
}, []);
Conclusion
Custom hooks are like building your own tools. The more you create, the easier your development becomes. Start by extracting repeated patterns in your code, then gradually build a collection of hooks you use across projects.
The hooks I showed you today are just the beginning. As you build more React apps, you'll discover patterns that deserve their own custom hooks. Don't be afraid to experiment!
Try It Yourself
Pick one of these challenges:
- Create a
useDebouncehook for search inputs - Build a
useOnClickOutsidehook for closing modals - Make a
useAsynchook for handling any async operation
Happy coding!