Custom Hooks and the Rules of Hooks in React
Custom Hooks and the Rules of Hooks
~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
To wrap up Phase 2: the two fixed rules that apply to ALL hooks, and how to build your own, reusable hooks by combining existing ones.
The two Rules of Hooks
- Only call hooks at the top level. Never inside
ifblocks, loops, or nested functions – React relies on the EXACT SAME ORDER of hook calls on every render to know which state belongs to whichuseStatecall. - Only call hooks from React function components or other custom hooks. Never from regular JavaScript functions or class components.
// WRONG – hook inside a condition:
function Example({ showCounter }) {
if (showCounter) {
const [count, setCount] = useState(0); // ✗ not allowed
}
// ...
}
// CORRECT – always call the hook, only condition the JSX:
function Example({ showCounter }) {
const [count, setCount] = useState(0); // ✓ always at the top level
return showCounter ? <p>{count}</p> : null;
}Achtung: If your code violates these rules, the "react-hooks/rules-of-hooks" ESLint rule (active by default in every Vite React project) flags it IMMEDIATELY on save in your editor – so you don't have to memorize these rules by heart, just understand WHY they exist.
What is a custom hook?
A "custom hook" is simply a completely normal JavaScript function that (a) starts with use (pure naming convention, but important – that's how React AND the ESLint rule recognize that this function is itself allowed to call other hooks) and (b) uses one or more built-in hooks inside. Recurring hook combinations can be neatly reused this way, instead of duplicating them in every component.
hooks/useDocumentTitle.js: building our first custom hook
Remember the document.title effect from chapter 8? Let's extract it into a reusable hook. Create src/hooks/useDocumentTitle.js:
import { useEffect } from 'react';
export function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}Note [title] instead of an empty array – this way the tab title automatically updates again whenever title changes, not just once on the first render. That's an improvement over the original from chapter 8.
Extending AuthContext.jsx: a useAuth hook
useContext(AuthContext) from chapter 11 can also be wrapped in its own hook – common in real React projects, because it (a) hides the AuthContext import itself from components and (b) throws a helpful error if someone accidentally calls useAuth() OUTSIDE of AuthProvider. Extend src/context/AuthContext.jsx:
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
function login(username) {
setUser({ username });
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth() must be used inside <AuthProvider>.');
}
return context;
}AuthContext itself is no longer exported (no more export before const AuthContext) – from the outside, only AuthProvider and useAuth get used, AuthContext has become a pure implementation detail of this file.
Updating App.jsx: using both custom hooks
import { useMemo, useRef, useState } from 'react';
import ProductCard from './components/ProductCard';
import { useAuth } from './context/AuthContext';
import { useDocumentTitle } from './hooks/useDocumentTitle';
const SAMPLE_PRODUCTS = [
{ sku: 'boots-01', name: 'Hiking Boots', price: 89.99, imageUrl: 'https://picsum.photos/seed/1/300' },
{ sku: 'backpack-01', name: 'Trekking Backpack', price: 59.5, imageUrl: 'https://picsum.photos/seed/2/300' },
{ sku: 'jacket-01', name: 'Rain Jacket', price: 74.0, imageUrl: 'https://picsum.photos/seed/3/300' },
];
function App() {
const { user, login, logout } = useAuth();
const [query, setQuery] = useState('');
const searchInputRef = useRef(null);
useDocumentTitle(`Product Catalog (${SAMPLE_PRODUCTS.length} products)`);
const filtered = SAMPLE_PRODUCTS.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
const mostExpensiveProduct = useMemo(() => {
return filtered.reduce(
(max, p) => (p.price > (max?.price ?? 0) ? p : max),
null
);
}, [filtered]);
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
</header>
<input
ref={searchInputRef}
type="text"
placeholder="Search products..."
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
{mostExpensiveProduct && (
<p>Most expensive product: {mostExpensiveProduct.name} (${mostExpensiveProduct.price.toFixed(2)})</p>
)}
<div className="product-grid">
{filtered.map((product) => (
<ProductCard
key={product.sku}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
onSelect={() => alert(product.name + ' tapped')}
/>
))}
</div>
</div>
);
}
export default App;Notice the missing useEffect/useContext import in App.jsx now – both are hidden inside our own hooks. App.jsx itself reads much more clearly as a result: "I use auth, I set a document title", without knowing the details. That wraps up Phase 2 (Hooks).
Project structure after this chapter
produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.jsx
├── App.jsx (now uses useAuth + useDocumentTitle)
├── index.css
├── context/
│ └── AuthContext.jsx (extended: useAuth hook)
├── hooks/
│ └── useDocumentTitle.js ← NEW
└── components/
└── ProductCard.jsxTipp: A good rule of thumb for WHEN a custom hook is worth it: as soon as you'd copy the same hook code (or the same hook combination) into a second component, that's a strong signal to extract it into a custom hook instead.