Custom Hooks
Custom Hooks
Level 4 — Advanced Hooks Reusable JavaScript functions that encapsulate stateful logic and built-in React hooks to share behavior across components.
1. Prerequisites
useEffectHook — The fundamental hook frequently composed inside custom hooks.- Rules of Hooks — The architectural guidelines governing custom hook implementations.
2. Term Category
Component Pattern (stateful logic abstraction): Custom Hooks are user-defined JavaScript functions that encapsulate stateful component logic, combining standard built-in hooks (useState, useEffect, useCallback, useRef) into clean, reusable abstractions. Unlike traditional utility helper functions, custom hooks have direct access to React's Fiber hook engine.
Architecturally, custom hooks allow developers to share stateful behavior across distinct UI components without altering component hierarchies or resorting to inheritance. Calling a custom hook in multiple components instantiates isolated local state for each component instance.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In early React applications, sharing stateful logic (such as managing window resize listeners, handling form inputs, or fetching API data with loading states) required complex patterns like Higher-Order Components (HOCs) or Render Props.
These older patterns suffered from distinct drawbacks:
- Wrapper Hell: Nesting multiple HOCs wrapped component trees in dozens of artificial wrapper DOM/JSX nodes.
- Implicit Props: Passing data via HOCs obscured where props originated, leading to naming collisions.
- Code Duplication: Copy-pasting
useStateanduseEffectblocks across<Navbar />,<Sidebar />, and<Modal />inflated codebase size.
React introduced Custom Hooks to solve this. By extracting hook calls into a function starting with use, developers can share stateful logic as easily as calling a standard JavaScript function.
Rules of Custom Hooks
- Name Prefix: Must start with lowercase
use(e.g.,useNetworkStatus,useFetch,useLocalStorage). This convention enables the React ESLint plugin to enforce the Rules of Hooks inside the function. - Hook Composition: Must invoke at least one built-in React hook (
useState,useEffect, etc.). - Isolated State: Each invocation creates completely independent state variables. Component A calling
useCounter()does NOT share counter state with Component B.
(2) Reality Metaphor
Imagine renting a standardized toolbox for home repairs.
- Without Custom Hooks (Buying Individual Tools): Every homeowner buys their own hammer, tape measure, screwdriver, and electric drill separately, organizing them manually on their own workbench.
- Custom Hook (Standardized Toolbox): A company packages the hammer, tape measure, and drill into a single portable "Home Repair Toolbox" (
useHomeRepair). Each homeowner rents their own independent toolbox. The tools work identically, but one homeowner driving a nail in their living room does not affect the other homeowner's wall.
(3) React Code Examples
Short Snippet
import { useState, useEffect } from 'react';
// Custom Hook encapsulating browser online status
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
Fuller Example
import React, { useState, useEffect } from 'react';
// Custom Hook for reusable API fetching with loading & error states
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCurrent = true;
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.then((json) => {
if (isCurrent) {
setData(json);
setError(null);
setLoading(false);
}
})
.catch((err) => {
if (isCurrent) {
setError(err.message);
setLoading(false);
}
});
return () => {
isCurrent = false;
};
}, [url]);
return { data, loading, error };
}
// Component consuming the custom hook
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading user profile...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
export default UserProfile;
4. Common Mistakes & Pitfalls
Mistake 1: Omitting the Mandatory use Prefix
The mistake: Naming a custom hook function function fetchUserData() instead of function useUserData().
Why it's wrong: The React Linter relies on the use prefix convention to enforce the Rules of Hooks. Without the prefix, the linter treats the function as a standard utility function and fails to catch illegal conditional hook calls.
Incorrect:
function getWindowDimensions() {
const [dim, setDim] = useState({ w: window.innerWidth }); // ❌ Lacks 'use' prefix!
return dim;
}
Fix:
function useWindowDimensions() {
const [dim, setDim] = useState({ w: window.innerWidth }); // ✅ Correct 'use' prefix
return dim;
}
Mistake 2: Assuming Custom Hooks Share State Across Component Instances
The mistake: Calling const counter = useCounter() in Component A and Component B expecting them to share a global counter value.
Why it's wrong: Custom hooks share stateful LOGIC, not state values! Each component instance that invokes a custom hook receives its OWN isolated state instance.
Incorrect:
// Expecting Component A and Component B to share custom hook state values automatically
Fix:
// Use Context API or global store if shared state across instances is required
Mistake 3: Returning Inline Objects Without Memoization in Custom Hooks
The mistake: Returning return { data, updateData } from a custom hook where updateData is an un-memoized inline function.
Why it's wrong: Consuming components that include the returned custom hook function in useEffect dependency arrays will suffer from infinite rendering loops due to changing function references.
Incorrect:
function useForm(initial) {
const [values, setValues] = useState(initial);
const reset = () => setValues(initial); // ❌ New reference every render!
return { values, reset };
}
Fix:
function useForm(initial) {
const [values, setValues] = useState(initial);
const reset = useCallback(() => setValues(initial), [initial]); // ✅ Stable reference
return { values, reset };
}
5. Practice Exercises
Exercise 1: IoT Telemetry Stream Custom Hook
Scenario: An industrial IoT dashboard displays telemetry for chemical reactors. Create a custom hook useReactorTelemetry(reactorId) that manages WebSocket streaming, returning { telemetry, isConnected }.
Requirements:
- Custom hook named
useReactorTelemetry. - Connect to WebSocket matching
reactorId. - Return
{ telemetry, isConnected }object. - Provide safe socket teardown on unmount or
reactorIdchange.
Answer
Implementation
import { useState, useEffect } from 'react';
export function useReactorTelemetry(reactorId) {
const [telemetry, setTelemetry] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
let active = true;
const ws = new WebSocket(`wss://telemetry.factory.com/reactors/${reactorId}`);
ws.onopen = () => {
if (active) setIsConnected(true);
};
ws.onmessage = (event) => {
if (active) {
setTelemetry(JSON.parse(event.data));
}
};
ws.onclose = () => {
if (active) setIsConnected(false);
};
return () => {
active = false;
ws.close();
};
}, [reactorId]);
return { telemetry, isConnected };
}
Technical Explanation
- Logic Encapsulation: WebSocket management is cleanly abstracted away from UI component JSX.
- Lifecycle Safety: Effect teardown closes sockets when
reactorIdupdates. - Isolated Instance: Consuming components receive dedicated telemetry state channels.
- Standard Naming:
useprefix guarantees linter rule enforcement.
Exercise 2: Financial Order Book Debounced Search Hook
Scenario: A stock trading desk inputs ticker queries. Create a custom hook useDebouncedValue(value, delay) to prevent spamming search queries on every keystroke.
Requirements:
- Custom hook
useDebouncedValue(value, delay). - Maintain local
debouncedValuestate. - Update
debouncedValueafter specifieddelayusingsetTimeout. - Clean up timers on
valueordelayupdates.
Answer
Implementation
import { useState, useEffect } from 'react';
export function useDebouncedValue(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Technical Explanation
- Timer Teardown:
clearTimeoutcancels pending state updates when fast typing occurs. - Generic Abstraction: Custom hook can debounce any value type across the application.
- Pure State Isolation: Operates independently of consuming component UI logic.
- Declarative Output: Returns stable debounced scalars for API queries.
Exercise 3: E-Commerce Session Storage Persisted State Hook
Scenario: An e-commerce checkout flow persists user selections in browser sessionStorage. Create a custom hook useSessionStorage(key, initialValue) behaving like useState.
Requirements:
- Custom hook returning
[storedValue, setStoredValue]. - Read initial value from
sessionStoragefallback toinitialValue. - Update
sessionStoragewheneverstoredValueupdates. - Support functional state updater callbacks.
Answer
Implementation
import { useState, useEffect, useCallback } from 'react';
export function useSessionStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
sessionStorage.setItem(key, JSON.stringify(storedValue));
} catch (err) {
console.error('SessionStorage set failed', err);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
Technical Explanation
- Lazy Initialization:
useState(() => ...)readssessionStoragesynchronously during initial render. - Automatic Synchronization:
useEffectpersists updates wheneverstoredValueshifts. - Array Tuple Return: Matches standard
useStatesignature conventions. - Error Resiliency:
try/catchguards prevent crashes in restricted iframe browser environments.
6. Related Terms
- Rules of Hooks — The architectural laws governing custom hook invocations.
useEffectHook — Built-in hook composed within custom hooks.useStateHook — Built-in state primitive composed within custom hooks.- Higher-Order Components (HOC) — Legacy pattern replaced by custom hooks.
7. Key Takeaways
- Custom Hooks extract stateful logic into reusable JavaScript functions starting with
use. - They allow sharing stateful behavior without modifying component tree hierarchies.
- Custom Hooks share stateful logic, NOT state values (each call creates isolated local state).
- Always prefix custom hooks with
useso linters can enforce Rules of Hooks. - Return tuples
[state, setter]or objects{ data, loading }tailored for consuming components.
---
## File 2: `knowledge-base/06-react/terms/level_04/forward_ref.md`
```markdown