Simplifying Toggle States in React: A Custom Hook Approach
In React applications, we often find ourselves implementing various toggle states - dark/light themes, online/offline status, modal visibility, or menu states. While useState
is the go-to solution for managing state, repeatedly writing similar toggle logic across components can lead to unnecessary boilerplate code. Let's explore how we can streamline this common pattern using a custom hook.
The Common Pattern
You might have seen (or written) code that looks like this:
function Navbar() {
const [isDarkMode, setIsDarkMode] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleDarkMode = () => setIsDarkMode(prev => !prev);
const toggleMenu = () => setIsMenuOpen(prev => !prev);
return (
<nav>
<button onClick={toggleDarkMode}>
{isDarkMode ? '🌙' : '☀️'}
</button>
<button onClick={toggleMenu}>
{isMenuOpen ? '✕' : '☰'}
</button>
</nav>
);
}
This pattern repeats across components, leading to:
- Redundant state declarations
- Duplicate toggle logic
- Increased possibility for errors
- Less maintainable code
Enter useToggleState
Let's introduce a custom hook that encapsulates this toggle behavior:
import { useState, useCallback } from 'react';
export function useToggleState(initialState: boolean) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => {
setState((prev) => !prev);
}, []);
return [state, toggle] as const;
}
This simple yet powerful hook provides several benefits:
- Encapsulates toggle logic in one reusable place
- Returns a tuple with the current state and a toggle function
- Uses
useCallback
to maintain reference stability - TypeScript support out of the box
Practical Usage Examples
Let's see how this hook simplifies our components:
function EnhancedNavbar() {
const [isDarkMode, toggleDarkMode] = useToggleState(false);
const [isMenuOpen, toggleMenu] = useToggleState(false);
return (
<nav>
<button onClick={toggleDarkMode}>
{isDarkMode ? '🌙' : '☀️'}
</button>
<button onClick={toggleMenu}>
{isMenuOpen ? '✕' : '☰'}
</button>
</nav>
);
}
function OnlineStatus() {
const [isOnline, toggleOnline] = useToggleState(true);
return (
<div className={isOnline ? 'status-online' : 'status-offline'}>
{isOnline ? 'Connected' : 'Offline'}
</div>
);
}
function Modal() {
const [isVisible, toggleVisibility] = useToggleState(false);
return (
<>
<button onClick={toggleVisibility}>
{isVisible ? 'Hide Modal' : 'Show Modal'}
</button>
{isVisible && <div className="modal">Modal Content</div>}
</>
);
}
Advanced Usage: Adding Features
We can extend our hook to support more features while maintaining its simplicity:
interface ToggleOptions {
onToggle?: (newState: boolean) => void;
disabled?: boolean;
}
function useToggleState(initialState: boolean, options?: ToggleOptions) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => {
if (options?.disabled) return;
setState(prev => {
const newState = !prev;
options?.onToggle?.(newState);
return newState;
});
}, [options?.disabled, options?.onToggle]);
return [state, toggle] as const;
}
// Usage with callbacks
function ThemeToggle() {
const [isDarkMode, toggleTheme] = useToggleState(false, {
onToggle: (newState) => {
document.body.classList.toggle('dark-mode', newState);
},
});
return (
<button onClick={toggleTheme}>
Toggle Theme ({isDarkMode ? 'Dark' : 'Light'})
</button>
);
}
Best Practices and Considerations
When using useToggleState
, keep in mind:
- Initial State: Choose meaningful default values that match your UI's initial state
- Performance: The hook uses
useCallback
to prevent unnecessary rerenders - Type Safety: TypeScript support ensures correct usage across your application
- Naming: Use clear, descriptive names for your state variables (e.g.,
isVisible
instead of juststate
)
Conclusion
Custom hooks like useToggleState
demonstrate the power of React's composition model. By extracting common patterns into reusable hooks, we can:
- Reduce boilerplate code
- Improve code maintainability
- Ensure consistent behavior across components
- Make our components more focused and readable
Remember, while this hook is simple, it represents a broader pattern of identifying repeated logic in your React applications and extracting it into reusable hooks. This approach not only makes your code more maintainable but also helps establish consistent patterns across your team's codebase.