Welcome back, intrepid React developer! In previous chapters, we’ve explored the fundamental built-in Hooks like useState and useEffect, which revolutionized how we manage state and side effects in functional components. You’ve seen how powerful they are for managing component-specific logic.
But what happens when you find yourself writing the same useState and useEffect logic in multiple components? Perhaps you have several components that all need to fetch data from a similar API endpoint, or they all need to manage a toggle state with similar side effects. Copy-pasting code is a common anti-pattern that leads to “boilerplate” and makes your application harder to maintain.
That’s where Custom Hooks come in! This chapter will unlock the secret to creating your own reusable pieces of stateful logic. By the end, you’ll be able to encapsulate complex behaviors into simple, composable functions, making your React applications cleaner, more efficient, and a joy to maintain. Get ready to level up your React skills!
The Challenge of Duplication: Why Custom Hooks?
Imagine you’re building an application with several components that need to display user status (online/offline). Each component might independently manage this state, perhaps using useState and useEffect to subscribe to a real-time service or simply to update the status based on some event.
Here’s a simplified example of how two components might manage a simple isOnline state:
// components/FriendStatus.jsx
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
// Simulate subscribing to a chat API
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// Assume ChatAPI.subscribeToFriendStatus and ChatAPI.unsubscribeFromFriendStatus exist
// For this example, let's just set it after a delay
const timeoutId = setTimeout(() => {
setIsOnline(true); // Pretend friend is online
}, 1000);
return () => {
clearTimeout(timeoutId);
// ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, []); // Empty dependency array means this runs once on mount
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
export default FriendStatus;
Now, imagine another component, UserList, also needs to display the online status for multiple users. You’d find yourself repeating the useState and useEffect pattern. This leads to:
- Code Duplication (DRY principle violation): More code to write, more places to update if logic changes.
- Increased Complexity: Harder to read and understand the intent of each component.
- Maintenance Headaches: Bug fixes or feature additions require changes in multiple places.
Think about it: What if the logic for determining “online” changes? You’d have to update every single component that uses this logic. That’s not ideal, right?
What is a Custom Hook?
A Custom Hook is simply a JavaScript function whose name starts with use and that calls other Hooks (like useState, useEffect, useContext, etc.) internally. It’s a mechanism for extracting reusable stateful logic from components.
The “custom” part means you define it, rather than it being provided by React directly. It allows you to package up common logic into a single, standalone function that can be shared across different components.
Key characteristics:
- Naming Convention: It must start with the word
use(e.g.,useFriendStatus,useFetchData,useToggle). This convention is crucial because React relies on it to enforce the Rules of Hooks. - Calls other Hooks: Inside a custom hook, you can use any of the built-in React Hooks. This is where the “stateful logic” comes from.
- Returns values: A custom hook can return any values (state, functions, objects) that the consuming component needs.
- No JSX: Custom hooks are pure JavaScript functions; they don’t return JSX. They provide the logic that components use to render JSX.
Why the use prefix?
This is not just a suggestion; it’s a strict rule enforced by React. It helps React’s linter plugins identify components and hooks, ensuring that the Rules of Hooks (e.g., calling hooks only at the top level of a function component or another hook) are followed correctly. Without it, React wouldn’t know to check for these rules, potentially leading to subtle bugs.
Anatomy of a Custom Hook: Let’s Build One!
Let’s refactor our FriendStatus example into a custom hook. We’ll create a new file, say hooks/useFriendStatus.js, to encapsulate the isOnline logic.
Step 1: Identify the Reusable Logic
From our FriendStatus component, the reusable part is:
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
// Logic to determine friend's online status
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// Simulate subscription
const timeoutId = setTimeout(() => {
setIsOnline(true); // Pretend friend is online
}, 1000);
return () => {
clearTimeout(timeoutId);
// Simulate unsubscription
};
}, []);
This logic depends on a friendId (or similar identifier) to know whose status to track.
Step 2: Create a New JavaScript File for Your Hook
Create a new file, for instance, src/hooks/useFriendStatus.js.
// src/hooks/useFriendStatus.js
import { useState, useEffect } from 'react';
// For demonstration, we'll create a mock ChatAPI
// In a real app, this would be an actual API or WebSocket client
const ChatAPI = {
subscribeToFriendStatus: (friendId, callback) => {
console.log(`Subscribing to friend ${friendId} status...`);
// Simulate async data fetching
const isOnline = friendId % 2 === 0; // Even IDs are online, odd are offline
const timeout = setTimeout(() => {
callback({ isOnline });
}, 1000);
return timeout; // Return the timeout ID for cleanup
},
unsubscribeFromFriendStatus: (friendId, timeoutId) => {
console.log(`Unsubscribing from friend ${friendId} status.`);
clearTimeout(timeoutId);
}
};
function useFriendStatus(friendId) {
// We'll put our state and effect logic here
}
export default useFriendStatus;
Notice we’ve added a mock ChatAPI to make our example self-contained. In a real application, this would be an external service.
Step 3: Move the Logic into the Custom Hook
Now, let’s move the useState and useEffect logic into useFriendStatus. It should accept friendId as an argument, as the logic depends on it.
// src/hooks/useFriendStatus.js
import { useState, useEffect } from 'react';
// Mock ChatAPI (as defined above)
const ChatAPI = {
subscribeToFriendStatus: (friendId, callback) => {
console.log(`Subscribing to friend ${friendId} status...`);
const isOnline = friendId % 2 === 0;
const timeout = setTimeout(() => {
callback({ isOnline });
}, 1000);
return timeout;
},
unsubscribeFromFriendStatus: (friendId, timeoutId) => {
console.log(`Unsubscribing from friend ${friendId} status.`);
clearTimeout(timeoutId);
}
};
function useFriendStatus(friendId) { // Accepts friendId as an argument
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
if (!friendId) { // Handle cases where friendId might be undefined
setIsOnline(null);
return;
}
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
const subscriptionId = ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, subscriptionId);
};
}, [friendId]); // IMPORTANT: dependency array includes friendId
return isOnline; // Return the state value
}
export default useFriendStatus;
Explanation of the Custom Hook:
function useFriendStatus(friendId): This defines our custom hook. It takesfriendIdas an argument, making it dynamic.const [isOnline, setIsOnline] = useState(null);: Just like in a component, we declare state within the hook. This state is isolated for each component that usesuseFriendStatus.useEffect(() => { ... }, [friendId]);: The side effect logic lives here.- It subscribes to the
ChatAPIusing the providedfriendId. - The
handleStatusChangecallback updates theisOnlinestate. - The cleanup function
return () => { ... }unsubscribes when the component unmounts orfriendIdchanges. - Crucially,
[friendId]is in the dependency array. This ensures the effect re-runs if thefriendIdchanges, correctly subscribing to the new friend’s status. If you forget this, you’ll have stale subscriptions!
- It subscribes to the
Step 4: Using the Custom Hook in Components
Now, let’s update our FriendStatus component to use our new custom hook.
// src/components/FriendStatus.jsx
import React from 'react';
import useFriendStatus from '../hooks/useFriendStatus'; // Import our custom hook
function FriendStatus(props) {
// Use the custom hook! It returns the isOnline status.
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
export default FriendStatus;
Wow! Look how much cleaner FriendStatus is now! All the subscription/unsubscription logic is neatly tucked away in useFriendStatus.
Now, let’s create another component, ChatRecipientPicker, that also needs to know if a selected friend is online.
// src/components/ChatRecipientPicker.jsx
import React, { useState } from 'react';
import useFriendStatus from '../hooks/useFriendStatus'; // Reusing the same hook!
// Mock list of friends
const friendList = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
];
function ChatRecipientPicker() {
const [recipientId, setRecipientId] = useState(1); // Default to Alice
// Use the custom hook for the currently selected recipient
const isRecipientOnline = useFriendStatus(recipientId);
return (
<div>
<label>Choose a friend:</label>
<select
value={recipientId}
onChange={e => setRecipientId(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
{isRecipientOnline === null ? (
<span> Loading status...</span>
) : (
<span> {isRecipientOnline ? '✅ Online' : '❌ Offline'}</span>
)}
</div>
);
}
export default ChatRecipientPicker;
Notice how both FriendStatus and ChatRecipientPicker use the exact same useFriendStatus hook, but each component gets its own independent state for isOnline. This is the magic of custom hooks: they share the logic, not the state itself. Every time you call a custom hook, it creates an isolated instance of the state and effects within that component.
To see this in action, you’d integrate these components into your App.jsx:
// src/App.jsx
import React from 'react';
import FriendStatus from './components/FriendStatus';
import ChatRecipientPicker from './components/ChatRecipientPicker';
function App() {
const friend = { id: 1, name: 'Alice' }; // Example friend for FriendStatus
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Custom Hooks Demo</h1>
<h2>Friend Status Component:</h2>
<p>Alice is: <FriendStatus friend={friend} /></p>
<p>Bob is: <FriendStatus friend={{ id: 2, name: 'Bob' }} /></p> {/* Another instance */}
<hr />
<h2>Chat Recipient Picker:</h2>
<ChatRecipientPicker />
<p style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
(Remember: Even IDs are online, Odd IDs are offline in our mock API!)
</p>
</div>
);
}
export default App;
Run your React application (npm start or yarn start). You’ll see both components independently displaying online/offline status, driven by the single useFriendStatus custom hook!
Mini-Challenge: Build a useToggle Hook
You often need a simple boolean state that can be toggled. Instead of writing const [isOpen, setIsOpen] = useState(false); const toggleOpen = () => setIsOpen(prev => !prev); repeatedly, let’s create a custom hook for it!
Challenge:
Create a custom hook called useToggle that:
- Takes an optional initial boolean value (defaults to
false). - Returns the current boolean state and a
togglefunction to flip its value. - Demonstrate its use in a component that shows/hides some content.
Hint: You’ll need useState inside your custom hook. The toggle function should update this state.
What to observe/learn: How easily you can abstract simple state logic into a reusable hook, making your components much cleaner.
Click for Solution Hint!
Think about what useState gives you. You need to return both the state value and a function to update it. The toggle function will call the state updater with a callback that flips the previous state.
Click for Solution!
src/hooks/useToggle.js:
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// useCallback is used here to memoize the toggle function.
// This prevents unnecessary re-renders of child components
// that receive `toggle` as a prop, as its reference won't change
// unless `setValue` itself changes (which it doesn't).
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []); // Empty dependency array means this function is stable
return [value, toggle];
}
export default useToggle;
src/components/ToggleContent.jsx (or directly in App.jsx):
import React from 'react';
import useToggle from '../hooks/useToggle'; // Import your new hook
function ToggleContent() {
// Use the custom hook, with an initial value of true
const [isVisible, toggleVisibility] = useToggle(true);
return (
<div>
<button onClick={toggleVisibility}>
{isVisible ? 'Hide Content' : 'Show Content'}
</button>
{isVisible && (
<div style={{ border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}>
<h3>This is the toggled content!</h3>
<p>It can be anything you want to show or hide.</p>
</div>
)}
</div>
);
}
export default ToggleContent;
Then add <ToggleContent /> to your App.jsx.
Common Pitfalls & Troubleshooting
Forgetting the
usePrefix:- Pitfall: Naming your custom hook
friendStatusinstead ofuseFriendStatus. - Problem: React’s linter won’t recognize it as a hook, and you might get errors about “Hooks can only be called inside of the body of a functional component” or “React Hook ‘useState’ is called in function ‘friendStatus’ which is neither a React function component nor a custom React Hook function.”
- Solution: Always prefix your custom hooks with
use.
- Pitfall: Naming your custom hook
Incorrect Dependency Array in
useEffect(within the hook):- Pitfall: Forgetting to include all values that the
useEffectcallback depends on in its dependency array. In ouruseFriendStatusexample, if we haduseEffect(() => { ... }, []);instead of[friendId]. - Problem: The effect will only run once on mount. If
friendIdchanges, the hook will continue to use the stalefriendIdfrom the initial render, leading to incorrect behavior (e.g., subscribing to the wrong friend). - Solution: Carefully list all external variables (props, state, or functions defined outside the effect but used inside) that the
useEffectcallback relies on in its dependency array. Useeslint-plugin-react-hookswhich often catches these automatically.
- Pitfall: Forgetting to include all values that the
Returning JSX from a Custom Hook:
- Pitfall: Trying to return
<p>Hello from custom hook</p>directly fromuseMyHook(). - Problem: Custom hooks are for logic, not rendering. They should return data, functions, or objects, which components then use to render JSX.
- Solution: Ensure your custom hook returns values that the component can then render.
- Pitfall: Trying to return
Breaking the Rules of Hooks within a Custom Hook:
- Pitfall: Calling
useStateoruseEffectinside a conditional (if) statement or loop within your custom hook. - Problem: This violates the fundamental Rules of Hooks and will lead to unpredictable behavior and errors, as React relies on a consistent order of hook calls.
- Solution: Always call Hooks at the top level of your custom hook function.
- Pitfall: Calling
Summary
Congratulations! You’ve successfully learned how to create and utilize custom React Hooks. This is a crucial skill for building scalable and maintainable React applications.
Here are the key takeaways from this chapter:
- Custom Hooks solve duplication: They allow you to extract and reuse stateful logic that appears in multiple components.
- Naming is key: Custom hooks must start with the
useprefix (e.g.,useMyAwesomeLogic). - They call other Hooks: Inside a custom hook, you use built-in Hooks like
useStateanduseEffect. - They share logic, not state: Each component calling a custom hook gets its own isolated instance of the state and effects managed by that hook.
- They return values: Custom hooks return data, functions, or objects that the consuming component uses. They do not return JSX.
- Adhere to Rules of Hooks: Custom hooks must follow the same rules as built-in hooks (top-level calls, no conditionals).
- Dependency arrays are vital: Pay close attention to
useEffectdependency arrays within your custom hooks to prevent stale closures and ensure correct behavior.
What’s next?
Now that you’re comfortable with custom hooks, we’re going to dive deeper into more advanced state management patterns. In the next chapter, we’ll explore the useReducer Hook, a powerful alternative to useState for managing more complex state logic, especially when state updates depend on previous state or involve multiple related values. This will further enhance your ability to write robust and predictable React applications.
References
- React Official Documentation - Building Your Own Hooks: https://react.dev/learn/reusing-logic-with-custom-hooks
- React Official Documentation - Rules of Hooks: https://react.dev/warnings/rules-of-hooks
- MDN Web Docs - React custom hooks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions/React_custom_hooks
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.