Build Your First Custom Hook: useToggle
Custom hooks are functions that extract stateful logic into reusable, encapsulated packages. The useToggle hook is the perfect starting point: it eliminates the repetitive pattern of managing boolean state for modals, dropdowns, and visibility toggles across your application.
Key Takeaways
- Custom hooks are JavaScript functions that encapsulate reusable React logic
useToggleeliminates the need to writeconst [value, setValue] = useState(false); const toggle = () => setValue(!value)repeatedly- Custom hooks must be imported and called in functional components like any built-in hook
useCallbackmemoizes the toggle function to prevent unnecessary re-renders in optimized children- Custom hooks return values in a predictable format (often matching built-in hook patterns)
- A single
useTogglehook can replace dozens of similar state variables across your codebase
The Problem: Repetitive Toggle Pattern
The same pattern appears everywhere:
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
This works for modals, dropdowns, accordions, and visibility toggles. Writing it dozens of times violates the DRY (Don't Repeat Yourself) principle. A custom hook extracts this logic once, then reuses it everywhere.
Building useToggle from Scratch
Create a new file useToggle.js:
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
export default useToggle;
Breaking it down:
function useToggle(initialValue = false)— The hook accepts an optional starting value (defaults tofalse)const [value, setValue] = useState(initialValue)— Uses React'suseStateto manage the booleanconst toggle = useCallback(() => { setValue(v => !v); }, [])— Creates a memoized toggle function.useCallbackprevents recreating the function on every render (important for performance when passing to optimized children). The[]dependency array means this function is created once.return [value, toggle]— Returns both the state value and the toggle function as an array, matching theuseStatepattern
Using useToggle in a Component
Here's a complete example of a modal that toggles visibility:
import React from 'react';
import useToggle from './useToggle';
function ModalExample() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>
{isOpen ? 'Close Modal' : 'Open Modal'}
</button>
{isOpen && (
<div style={{ border: '1px solid #ccc', padding: '1rem' }}>
<h2>Modal Content</h2>
<p>This modal is now visible!</p>
<button onClick={toggleOpen}>Dismiss</button>
</div>
)}
</div>
);
}
export default ModalExample;
How it works:
const [isOpen, toggleOpen] = useToggle(false)— Destructure the returned array intoisOpen(boolean) andtoggleOpen(function)onClick={toggleOpen}— Clicking calls the toggle function, flippingisOpento the opposite value{isOpen && ...}— Conditionally render the modal based on the boolean state
Why useCallback Matters
useCallback memoizes the toggle function. Without it, React recreates the function on every render:
// WITHOUT useCallback - function recreated each render
const toggle = () => {
setValue(v => !v);
};
// WITH useCallback - function created once, reused forever
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
In this case, the dependency array [] is empty because toggle has no dependencies. If you use variables from the component scope, add them to the array:
// If toggle logic depended on something
const toggle = useCallback(() => {
setValue(!value); // depends on 'value'
}, [value]); // add to dependency array
Memoization prevents re-rendering in child components wrapped with React.memo.
Real-World Examples
Example 1: Accordion Toggle
function AccordionItem({ title, content }) {
const [isExpanded, toggleExpand] = useToggle(false);
return (
<div>
<button onClick={toggleExpand} style={{ fontSize: '1.2rem' }}>
{isExpanded ? '▼' : '▶'} {title}
</button>
{isExpanded && <p>{content}</p>}
</div>
);
}
Example 2: Form Visibility
function SettingsPanel() {
const [showAdvanced, toggleAdvanced] = useToggle(false);
return (
<div>
<button onClick={toggleAdvanced}>
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</button>
{showAdvanced && (
<div>
<label>API Key: <input /></label>
<label>Timeout: <input /></label>
</div>
)}
</div>
);
}
Example 3: Dark Mode Toggle
function ThemeToggle() {
const [isDark, toggleTheme] = useToggle(false);
const bgColor = isDark ? '#1a1a1a' : '#ffffff';
const textColor = isDark ? '#ffffff' : '#000000';
return (
<div style={{ backgroundColor: bgColor, color: textColor, padding: '1rem' }}>
<button onClick={toggleTheme}>
{isDark ? '☀️ Light Mode' : '🌙 Dark Mode'}
</button>
<p>Current theme: {isDark ? 'Dark' : 'Light'}</p>
</div>
);
}
Frequently Asked Questions
Can I set a specific value instead of toggling?
Yes, create an extended version:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
const set = useCallback((val) => {
setValue(val);
}, []);
return [value, toggle, set]; // Return a third function
}
// Usage:
const [isOpen, toggle, setIsOpen] = useToggle(false);
setIsOpen(true); // Set directly
toggle(); // Toggle
What's the difference between a custom hook and a regular function?
Custom hooks use React hooks (like useState) inside them. Regular functions cannot call hooks. React requires hooks to be called at the top level of components or inside custom hooks.
Can I call a custom hook from another custom hook?
Yes! Hooks can call other hooks. This enables composition:
function useModal() {
const [isOpen, toggleOpen] = useToggle(false);
const [content, setContent] = useState('');
return { isOpen, toggleOpen, content, setContent };
}
Should I always use useCallback in custom hooks?
Not always. Use useCallback when:
- The returned function is passed to optimized children (
React.memo) - The function is in a dependency array (triggers re-runs if not memoized)
- Performance profiling shows unnecessary re-renders
For simple hooks like useToggle, it's good practice even if not always strictly necessary.
Why does useCallback have an empty dependency array?
The toggle function never depends on external values. It only reads the current state via setValue(v => !v), where v is provided by setState. If you use external variables, add them to the array:
// If toggle depended on a prop (bad design, but possible):
const toggle = useCallback(() => {
setValue(someExternalValue);
}, [someExternalValue]); // Add dependency
Further Reading
- React Official: Custom Hooks — Official guide to building custom hooks
- React Official: useCallback — When and why to memoize callbacks
- React Official: Rules of Hooks — Requirements for calling hooks