Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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

  1. Only call hooks at the top level. Never inside if blocks, loops, or nested functions – React relies on the EXACT SAME ORDER of hook calls on every render to know which state belongs to which useState call.
  2. 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:

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:

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

src/App.jsx
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.jsx

Tipp: 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.