Learn how to use custom hooks to achieve code reuse and clean code in React. Discover how to identify extractable logic and follow the Rules of Hooks.
Previously in this course, we explored how to optimize our components by managing function stability with useCallback and preventing unnecessary re-renders using useMemo. While those tools keep our components performant, our codebase often suffers from another issue: logic duplication.
In this lesson, we move beyond built-in hooks and start building our own. By learning how to implement custom hooks, you’ll gain the ability to move complex, recurring logic out of your UI components and into reusable, testable functions.
In a dashboard application, you’ll frequently find yourself writing the same state management logic across multiple components. Perhaps you're managing a "loading" toggle, tracking window dimensions, or syncing form fields with internal state.
If you copy-paste that logic, you create a maintenance nightmare. If you need to change how that logic works later, you have to find and update it in five different places. Custom hooks provide a way to perform abstraction, allowing you to encapsulate that logic and share it across your entire application. Think of this as the React equivalent of the Laravel helpers: How to build and use custom global functions pattern—it's about making your core logic reusable and clean.
A good candidate for a custom hook is any logic that uses one or more built-in hooks (useState, useEffect, useRef, etc.) and repeats across your project.
Look for these "code smells" in your components:
useEffect setups (e.g., event listeners or API subscriptions).useState logic is so dense it obscures the actual UI rendering.useToggle HookLet's look at a common pattern: toggling a boolean value, like a modal visibility state or a "dark mode" switch.
JSXfunction SettingsModal() { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(prev => !prev); return <button onClick={toggle}>{isOpen ? CE9178">'Close' : CE9178">'Open'}</button>; }
If we have three different components that need this, we're repeating useState and the toggle function.
We can extract this into a function named useToggle. By convention, custom hooks must start with the word use.
JAVASCRIPT// hooks/useToggle.js import { useState } from CE9178">'react'; export function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = () => setValue((prev) => !prev); return [value, toggle]; }
Now, our component becomes significantly cleaner:
JSXimport { useToggle } from CE9178">'./hooks/useToggle'; function SettingsModal() { const [isOpen, toggle] = useToggle(false); return <button onClick={toggle}>{isOpen ? CE9178">'Close' : CE9178">'Open'}</button>; }
This is the essence of Refactoring for Clean Code: Improving React Maintainability. We have successfully abstracted the state management away from the UI.
Because custom hooks use built-in hooks under the hood, they are bound by the same Rules of Hooks:
In our dashboard project, we often need to track if a component is currently "hovered."
hooks/useHover.js.useState to track a boolean isHovered.useRef to target the element.useEffect to attach mouseover and mouseout event listeners to that element.isHovered boolean and the ref object.Hint: Remember that useEffect cleanup is essential to remove those event listeners when the component unmounts.
toggle instead of useToggle, React’s linting tools won't know it's a hook, and you won't get warnings if you violate the Rules of Hooks. Always prefix with use.useEffect with dependencies, ensure those dependencies are passed correctly. If you're struggling with stale data, revisit Persistent Mutable Values with useRef.Custom hooks are the primary mechanism for code reuse in modern React. By extracting stateful logic, you keep your components focused on rendering UI rather than managing complex side effects. As you continue to build your dashboard, look for opportunities to turn repeated logic into clean, reusable abstractions.
Up next: We will apply these principles to create a persistent useLocalStorage hook, enabling our dashboard settings to survive page reloads.
Learn to build a robust useLocalStorage hook. Master generic storage, JSON serialization, and cross-tab synchronization to keep your React app state persistent.
Read moreStop overwhelming your server with every keystroke. Learn how to implement debouncing in React to optimize API performance and create snappier UIs.
Introduction to Custom Hooks
Introduction to Context API
Architecting Global State with Context and Reducer
Implementing Theme Context
Structuring State for Performance
Handling Authentication State
Integrating Reducers with Auth State
Introduction to React Router
Dynamic Routing with URL Parameters
Nested Routes and Layouts
Protected Routes for Authenticated Views
Programmatic Navigation
Building the Dashboard Navigation Structure
Asynchronous Data Lifecycle
Caching Strategies with React Query
Mutations and Data Updates
Synchronizing Client and Server State
Integrating Live Data into the Dashboard
Error Handling and Loading UI
Controlled vs Uncontrolled Components
Real-time Form Validation
Schema-based Validation with Zod
Handling Multi-step Forms
Optimizing Form Submissions
Performance Profiling with React DevTools
Refactoring for Scalability
Finalizing Dashboard Data Flow
Deploying the Application
Advanced Hook Composition
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections