useToggle Hook: Boolean State Simplification in React
Boolean state appears everywhere in React—visible or hidden, enabled or disabled, dark mode or light mode, menu open or closed. The standard useState(false) works, but it creates repetitive boilerplate: defining state, remembering to negate the current value, and writing setter logic. The useToggle hook collapses this pattern into a single line, turning verbose state management into clean, intention-revealing code.
While simple on the surface, a well-designed useToggle hook teaches you how React's hooks handle state, effects, and callbacks. It's an excellent foundation for understanding more complex hooks and a practical utility you'll use in nearly every component you build.
Table of Contents
- Why Boolean State Needs Simplification
- Basic useToggle Implementation
- Advanced Patterns: Callbacks and Initial Values
- Composition with Other Hooks
- Practical Application Scenarios
- Performance Considerations
- FAQ
Why Boolean State Needs Simplification
Consider a simple dark mode toggle:
// ❌ Repetitive: What should be simple is verbose
function DarkModeToggle() {
const [isDark, setIsDark] = useState(false);
return (
<button onClick={() => setIsDark(!isDark)}>
{isDark ? '☀️ Light' : '🌙 Dark'}
</button>
);
}
This is fine for a single toggle, but when you have multiple toggles, the pattern repeats:
// ❌ Pattern repetition becomes obvious
function UserSettings() {
const [isDark, setIsDark] = useState(false);
const [isNotified, setIsNotified] = useState(true);
const [isPublic, setIsPublic] = useState(false);
return (
<>
<label>
Dark mode:
<input
type="checkbox"
checked={isDark}
onChange={() => setIsDark(!isDark)}
/>
</label>
<label>
Notifications:
<input
type="checkbox"
checked={isNotified}
onChange={() => setIsNotified(!isNotified)}
/>
</label>
<label>
Public profile:
<input
type="checkbox"
checked={isPublic}
onChange={() => setIsPublic(!isPublic)}
/>
</label>
</>
);
}
The pattern is clear: set state, negate current value, repeat. A useToggle hook eliminates this boilerplate while making the intent explicit.
Basic useToggle Implementation
TypeScript Version
import { useState, useCallback } from 'react';
interface UseToggleReturn {
// Current boolean value
value: boolean;
// Toggle to opposite value
toggle: () => void;
// Set to specific value
setValue: (value: boolean) => void;
// Set to true
on: () => void;
// Set to false
off: () => void;
}
export function useToggle(initialValue: boolean = false): UseToggleReturn {
const [value, setValue] = useState(initialValue);
// Toggle to opposite value
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []);
// Set to true
const on = useCallback(() => {
setValue(true);
}, []);
// Set to false
const off = useCallback(() => {
setValue(false);
}, []);
return {
value,
toggle,
setValue,
on,
off,
};
}
JavaScript Version
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []);
const on = useCallback(() => {
setValue(true);
}, []);
const off = useCallback(() => {
setValue(false);
}, []);
return {
value,
toggle,
setValue,
on,
off,
};
}
Usage Example
Now the repetitive boilerplate disappears:
// ✅ Clean and intention-revealing
function UserSettings() {
const darkMode = useToggle(false);
const notifications = useToggle(true);
const publicProfile = useToggle(false);
return (
<>
<label>
Dark mode:
<input
type="checkbox"
checked={darkMode.value}
onChange={darkMode.toggle}
/>
</label>
<label>
Notifications:
<input
type="checkbox"
checked={notifications.value}
onChange={notifications.toggle}
/>
</label>
<label>
Public profile:
<input
type="checkbox"
checked={publicProfile.value}
onChange={publicProfile.toggle}
/>
</label>
</>
);
}
Advanced Patterns: Callbacks and Initial Values
Toggle with Lifecycle Callbacks
interface UseToggleAdvancedOptions {
initialValue?: boolean;
// Called when toggled on
onOn?: () => void;
// Called when toggled off
onOff?: () => void;
// Called on any change
onChange?: (value: boolean) => void;
}
export function useToggleAdvanced(
options: UseToggleAdvancedOptions = {}
): UseToggleReturn {
const {
initialValue = false,
onOn,
onOff,
onChange,
} = options;
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(prev => {
const newValue = !prev;
// Call appropriate callback
if (newValue) {
onOn?.();
} else {
onOff?.();
}
onChange?.(newValue);
return newValue;
});
}, [onOn, onOff, onChange]);
const on = useCallback(() => {
setValue(prev => {
if (prev) return prev; // Already on
onOn?.();
onChange?.(true);
return true;
});
}, [onOn, onChange]);
const off = useCallback(() => {
setValue(prev => {
if (!prev) return prev; // Already off
onOff?.();
onChange?.(false);
return false;
});
}, [onOff, onChange]);
const setValue_direct = useCallback((newValue: boolean) => {
setValue(prev => {
if (prev === newValue) return prev; // No change
if (newValue) {
onOn?.();
} else {
onOff?.();
}
onChange?.(newValue);
return newValue;
});
}, [onOn, onOff, onChange]);
return {
value,
toggle,
setValue: setValue_direct,
on,
off,
};
}
JavaScript Version
export function useToggleAdvanced(options = {}) {
const {
initialValue = false,
onOn,
onOff,
onChange,
} = options;
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(prev => {
const newValue = !prev;
if (newValue) {
onOn?.();
} else {
onOff?.();
}
onChange?.(newValue);
return newValue;
});
}, [onOn, onOff, onChange]);
const on = useCallback(() => {
setValue(prev => {
if (prev) return prev;
onOn?.();
onChange?.(true);
return true;
});
}, [onOn, onChange]);
const off = useCallback(() => {
setValue(prev => {
if (!prev) return prev;
onOff?.();
onChange?.(false);
return false;
});
}, [onOff, onChange]);
const setValue_direct = useCallback((newValue) => {
setValue(prev => {
if (prev === newValue) return prev;
if (newValue) {
onOn?.();
} else {
onOff?.();
}
onChange?.(newValue);
return newValue;
});
}, [onOn, onOff, onChange]);
return {
value,
toggle,
setValue: setValue_direct,
on,
off,
};
}
Composition with Other Hooks
Toggle with useEffect
Control toggle state based on external conditions:
export function useToggleWithEffect(
initialValue: boolean = false,
shouldReset: boolean = false
) {
const toggle = useToggle(initialValue);
useEffect(() => {
if (shouldReset) {
toggle.off();
}
}, [shouldReset, toggle]);
return toggle;
}
// Usage: Reset menu when navigating
function NavigationMenu() {
const router = useRouter();
const menuOpen = useToggleWithEffect(false, router.pathname !== router.asPath);
return (
<>
<button onClick={menuOpen.toggle}>Menu</button>
{menuOpen.value && <nav>Navigation items</nav>}
</>
);
}
Toggle with LocalStorage
Persist toggle state:
export function useTogglePersisted(
key: string,
initialValue: boolean = false
) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
const toggle = useCallback(() => {
setValue(prev => {
const newValue = !prev;
localStorage.setItem(key, JSON.stringify(newValue));
return newValue;
});
}, [key]);
const on = useCallback(() => {
setValue(true);
localStorage.setItem(key, 'true');
}, [key]);
const off = useCallback(() => {
setValue(false);
localStorage.setItem(key, 'false');
}, [key]);
return { value, toggle, setValue, on, off };
}
// Usage: Dark mode preference persists across sessions
function App() {
const darkMode = useTogglePersisted('darkMode', false);
return (
<div style={{ background: darkMode.value ? '#000' : '#fff' }}>
<button onClick={darkMode.toggle}>
{darkMode.value ? 'Light' : 'Dark'} mode
</button>
</div>
);
}
Practical Application Scenarios
Scenario 1: Modal/Dialog Toggle
export function ModalDialog({
title,
children,
}: {
title: string;
children: ReactNode;
}) {
const modal = useToggle(false);
return (
<>
<button onClick={modal.on}>Open {title}</button>
{modal.value && (
<div className="modal-backdrop" onClick={modal.off}>
<div className="modal" onClick={e => e.stopPropagation()}>
<h2>{title}</h2>
{children}
<button onClick={modal.off}>Close</button>
</div>
</div>
)}
</>
);
}
// Usage
function App() {
return (
<>
<h1>My App</h1>
<ModalDialog title="Settings">
<p>Configure your preferences here.</p>
</ModalDialog>
</>
);
}
Scenario 2: Form Field Visibility
export function PasswordInput() {
const showPassword = useToggle(false);
return (
<div className="password-field">
<input
type={showPassword.value ? 'text' : 'password'}
placeholder="Enter password"
/>
<button onClick={showPassword.toggle}>
{showPassword.value ? 'Hide' : 'Show'}
</button>
</div>
);
}
Scenario 3: Accordion/Collapsible Section
export function AccordionItem({
title,
children,
}: {
title: string;
children: ReactNode;
}) {
const expanded = useToggle(false);
return (
<div className="accordion-item">
<button
className="accordion-header"
onClick={expanded.toggle}
aria-expanded={expanded.value}
>
<span>{title}</span>
<span className={`icon ${expanded.value ? 'open' : ''}`}>▼</span>
</button>
{expanded.value && (
<div className="accordion-content">{children}</div>
)}
</div>
);
}
// Usage
function FAQ() {
return (
<>
<AccordionItem title="What is this?">
<p>This is an accordion component.</p>
</AccordionItem>
<AccordionItem title="How does it work?">
<p>Click the header to expand or collapse.</p>
</AccordionItem>
</>
);
}
Scenario 4: Feature Flags and Experimental UI
export function FeatureFlag({
flag,
experimental,
production,
}: {
flag: boolean;
experimental: ReactNode;
production: ReactNode;
}) {
return flag ? experimental : production;
}
// In component
export function Dashboard() {
const betaUI = useToggle(false);
return (
<div>
<button onClick={betaUI.toggle}>
{betaUI.value ? 'Disable' : 'Enable'} Beta UI
</button>
<FeatureFlag
flag={betaUI.value}
production={<OldDashboard />}
experimental={<NewBetaDashboard />}
/>
</div>
);
}
Scenario 5: Loading State Toggle
export function useAsyncToggle<T>(
asyncFn: () => Promise<T>
) {
const loading = useToggle(false);
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
loading.on();
setError(null);
try {
const result = await asyncFn();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
loading.off();
}
}, [asyncFn, loading]);
return {
isLoading: loading.value,
data,
error,
execute,
};
}
// Usage
function DataFetcher() {
const { isLoading, data, error, execute } = useAsyncToggle(
() => fetch('/api/data').then(r => r.json())
);
return (
<>
<button onClick={execute} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Fetch Data'}
</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
</>
);
}
Scenario 6: Theme Toggle with Effects
export function ThemeToggle() {
const darkMode = useToggleAdvanced({
initialValue: false,
onOn: () => {
document.documentElement.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
},
onOff: () => {
document.documentElement.classList.remove('dark');
document.documentElement.style.colorScheme = 'light';
},
});
return (
<button onClick={darkMode.toggle}>
{darkMode.value ? '☀️ Light' : '🌙 Dark'}
</button>
);
}
Performance Considerations
useCallback for Stable Functions
The hook uses useCallback to ensure toggle functions maintain stable references:
// ✅ Stable reference across renders
const { toggle } = useToggle(false);
// If toggle wasn't memoized, this would cause unnecessary re-renders:
<ChildComponent onToggle={toggle} />
Avoiding Unnecessary Re-renders
Since toggle state is simple boolean, updates are inherently efficient. But when composing with other state:
export function useToggleWithDependents(
initialValue: boolean = false,
dependentState?: any
) {
const toggle = useToggle(initialValue);
// Only re-compute when toggle or dependent state changes
const computedValue = useMemo(
() => someExpensiveComputation(toggle.value, dependentState),
[toggle.value, dependentState]
);
return { ...toggle, computedValue };
}
FAQ
Q: Why not just use useState(!value) inline?
A: useToggle clarifies intent. When someone sees onClick={toggle}, it's immediately clear what happens. With onClick={() => setValue(!value)}, you need to read the implementation. useToggle is about code clarity and reusability.
Q: Should I memoize the toggle functions?
A: Yes, using useCallback is recommended when passing toggle functions to child components to prevent unnecessary re-renders. The basic implementation includes this.
Q: Can I use this with TypeScript?
A: Yes, the hook is fully typed. The interface clearly defines the return object shape.
Q: What if I need to toggle based on a condition?
A: Use the setValue method:
const isOpen = useToggle(false);
// Toggle based on condition
if (someCondition) {
isOpen.setValue(true);
} else {
isOpen.setValue(false);
}
Q: How do I test a component using useToggle?
A: Just test the behavior:
test('toggle button works', () => {
const { getByRole } = render(<ToggleButton />);
const button = getByRole('button');
expect(button).toHaveTextContent('Show');
act(() => {
button.click();
});
expect(button).toHaveTextContent('Hide');
});
Q: Can I combine multiple toggles?
A: Yes, just use multiple hooks:
function ComplexUI() {
const menu = useToggle(false);
const sidebar = useToggle(true);
const notifications = useToggle(false);
// Use all three independently
return (
<div>
<button onClick={menu.toggle}>Menu</button>
<button onClick={sidebar.toggle}>Sidebar</button>
<button onClick={notifications.toggle}>Notifications</button>
</div>
);
}
Q: Is useToggle in React by default?
A: No, you need to implement it or use a library. This hook is simple enough to copy into every project or publish as a reusable utility.
Common Patterns
Pattern 1: Controlled Toggle
export function ControlledToggle({
value,
onChange,
}: {
value: boolean;
onChange: (value: boolean) => void;
}) {
const toggle = useToggleAdvanced({
initialValue: value,
onChange,
});
return (
<button onClick={toggle.toggle}>
{toggle.value ? 'On' : 'Off'}
</button>
);
}
Pattern 2: Toggle with Reset
export function useToggleWithReset(
initialValue: boolean = false
) {
const toggle = useToggle(initialValue);
const reset = useCallback(() => {
toggle.setValue(initialValue);
}, [initialValue, toggle]);
return { ...toggle, reset };
}
Pattern 3: Timeout Toggle (Auto-reset)
export function useToggleWithTimeout(
initialValue: boolean = false,
timeoutMs: number = 5000
) {
const toggle = useToggle(initialValue);
const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);
const toggleWithTimeout = useCallback(() => {
toggle.toggle();
// Clear previous timeout
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
// Auto-reset after timeout
timeoutIdRef.current = setTimeout(() => {
toggle.setValue(initialValue);
}, timeoutMs);
}, [toggle, initialValue, timeoutMs]);
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
return { ...toggle, toggleWithTimeout };
}
Related Articles
- useState Hook Fundamentals
- State Management Patterns
- Toggle Switch Component
- useCallback Hook: Memoizing Functions
Next Steps
The useToggle hook is deceptively simple but foundational. It teaches you:
- How to abstract repetitive state patterns
- Why
useCallbackmatters for performance - How to compose hooks for richer behavior
- The power of intention-revealing code
From here, explore:
- Combining with
useReducerfor complex toggle logic - Building a
useAsynchook for loading states - Creating a custom form hook using multiple toggles
- Implementing a state machine for multi-step toggles
Start with the basic implementation in your projects. As your application scales, you'll appreciate the clarity and reusability this hook provides. Whether building a design system or a simple feature, useToggle is the foundation of clean boolean state management.
What patterns do you use toggles for most? Share your implementations in the comments—theme switching, modal management, and feature flags always spark interesting discussions about managing UI state elegantly.
Google AdSense Placeholder
CONTENT SLOT