Skip to main content

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
  • useToggle eliminates the need to write const [value, setValue] = useState(false); const toggle = () => setValue(!value) repeatedly
  • Custom hooks must be imported and called in functional components like any built-in hook
  • useCallback memoizes 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 useToggle hook 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:

  1. function useToggle(initialValue = false) — The hook accepts an optional starting value (defaults to false)
  2. const [value, setValue] = useState(initialValue) — Uses React's useState to manage the boolean
  3. const toggle = useCallback(() => { setValue(v => !v); }, []) — Creates a memoized toggle function. useCallback prevents recreating the function on every render (important for performance when passing to optimized children). The [] dependency array means this function is created once.
  4. return [value, toggle] — Returns both the state value and the toggle function as an array, matching the useState pattern

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 into isOpen (boolean) and toggleOpen (function)
  • onClick={toggleOpen} — Clicking calls the toggle function, flipping isOpen to 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