| |

React 36 ⚛️ Extracting Reusable Logic into Custom Hooks

React components often accumulate logic that has nothing to do with rendering. A component that fetches data, subscribes to a browser event, and manages a form field is doing three jobs at once. The rendering logic — the JSX — is buried under layers of state declarations and effect callbacks. Custom Hooks exist to pull that non-rendering logic out into named, testable functions that can be shared across components without duplicating a single line.

A custom Hook is not a new React feature in the sense of a new API. It is a convention built on top of the Hooks you already know. Any JavaScript function whose name starts with use and that calls other Hooks is a custom Hook . That simplicity is deceptive, because the convention unlocks a fundamental shift in how React code is organized. Instead of scattering useState and useEffect calls throughout component bodies, you compose behavior from named units that describe what they do: useData, useChatRoom, useOnlineStatus.

This chapter covers three areas. First, why custom Hooks exist and what problem they solve that components alone cannot. Second, how to extract and use custom Hooks — the mechanics of moving logic out of a component and the Rules of Hooks that govern where those functions can be called. Third, what separates a good custom Hook from a bad one — the naming, the focus, and the common traps that turn a useful abstraction into a layer of indirection. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing how custom Hooks fit into React’s rendering model.

Key point: A custom Hook is a JavaScript function whose name starts with use and that may call other Hooks. It lets you share stateful logic — not state itself — between components, keeping rendering in the component while the stateful machinery lives in one place .


Why Custom Hooks exist

The duplication problem. Before Hooks, sharing stateful logic between components required render props or higher-order components. Both patterns wrapped components in additional layers of the element tree, making the component hierarchy harder to read and the data flow harder to trace. If two components needed the same “fetch data with loading and error states” logic, you either duplicated the useState + useEffect code in both components, or you reached for a HOC that injected the data as props. The first approach meant bug fixes had to be applied in multiple places. The second meant every consumer component had an extra wrapper in the tree, and the injected props were not visible in the component’s own source .

The extraction problem. Custom Hooks solve duplication without wrapping. When two components share the same useState + useEffect pattern, that pattern itself is the abstraction . You move the pattern into a function named after its purpose, and both components call that function. The component no longer knows or cares how the data arrives — it just receives the result. This is why the React documentation describes custom Hooks as making “data flow explicit”: you feed the input in, you get the output out, and everything in between is hidden inside the Hook .

The composition problem. Hooks compose in a way that components do not. A component can render another component, but that creates a parent-child relationship in the tree. A Hook can call another Hook without adding anything to the tree. This means you can build complex behavior by combining smaller Hooks: a useChatRoom might internally use useData and useOnlineStatus, and the consuming component sees only useChatRoom. The composition is horizontal, not vertical .

The testing problem. Logic buried inside a component is hard to test in isolation. To test the fetch-and-track behavior, you need to render the entire component, mock its dependencies, and inspect the DOM. Logic inside a custom Hook can be tested as a function. You call the Hook in a test harness, assert on its return value, and verify its behavior without rendering any UI. This separation of concerns is not just stylistic — it changes what is possible to verify automatically .

The migration problem. React’s API evolves. useSyncExternalStore replaced a common pattern of useState + useEffect for subscribing to external stores. When a built-in solution arrives for a problem your custom Hook addresses, you can rewrite the Hook’s internals without changing a single consuming component . The component calls useOnlineStatus() regardless of whether that Hook is implemented with an effect or with useSyncExternalStore. The abstraction absorbs the change.

The trade-off. Extracting a custom Hook introduces a layer of indirection. The component no longer shows the useState and useEffect calls directly — it shows a function call. If the Hook is poorly named or does too little, that indirection makes the code harder to understand, not easier. A Hook that wraps a single useState with no derived behavior just renames the React primitive without adding meaning . The abstraction must earn its keep by encapsulating behavior that is either reused, complex, or both.


a. What makes a function a custom Hook

A custom Hook is defined by two properties: its name starts with use, and it calls at least one other Hook (built-in or custom). The naming convention is not optional. React’s linter uses the use prefix to identify functions that must follow the Rules of Hooks, and it uses the absence of that prefix to identify regular functions where Hooks are forbidden .

This is a minimal custom Hook. It does nothing useful, but it demonstrates the structure:

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

The function starts with use, calls useEffect, and has no return value. A component that calls useDocumentTitle('Home') gets the side effect of updating the document title whenever the title argument changes .

The Rules of Hooks apply to custom Hooks exactly as they apply to components. You cannot call useDocumentTitle inside a condition, a loop, or a nested function. You cannot call it from a regular JavaScript function that is not a component or another Hook. And you must call it at the top level of whatever function you are in, before any early returns .

The reason for these rules is the same reason they exist for built-in Hooks: React relies on call order to associate state with the correct Hook call. If you call a custom Hook conditionally, the internal useState and useEffect calls inside it will also be conditional, and React will lose track of which state belongs to which Hook .

b. Extracting a custom Hook from a component

The extraction process follows a predictable pattern. You identify a piece of stateful logic that could stand alone, move it into a function named after its purpose, and replace the original code with a call to that function.

Consider a component that fetches data from a URL. The component manages three pieces of state (data, error, and implicitly the loading state), runs an effect that fetches when the URL changes, and handles cleanup to prevent state updates after unmount.

function ProjectDashboard({ projectId }) {
  const [project, setProject] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch(`/api/projects/${projectId}`)
      .then((r) => r.json())
      .then((data) => {
        if (!cancelled) setProject(data);
      })
      .catch((e) => {
        if (!cancelled) setError(e);
      });
    return () => {
      cancelled = true;
    };
  }, [projectId]);

  if (error) return <ErrorBanner error={error} />;
  if (!project) return <Skeleton />;
  return <ProjectView project={project} />;
}

The fetch-loading-error pattern is not specific to projects. Any component that fetches data will repeat this structure. The extraction moves the pattern into a useFetch Hook that takes a URL and returns the data and error .

function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch(url)
      .then((r) => r.json())
      .then((value) => {
        if (!cancelled) setData(value);
      })
      .catch((e) => {
        if (!cancelled) setError(e);
      });
    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, error };
}

The component now calls useFetch and renders based on the returned values. The data flow is explicit: the URL goes in, the data and error come out .

function ProjectDashboard({ projectId }) {
  const { data: project, error } = useFetch(`/api/projects/${projectId}`);

  if (error) return <ErrorBanner error={error} />;
  if (!project) return <Skeleton />;
  return <ProjectView project={project} />;
}

The extraction does not change the behavior of the component. The same fetch runs, the same cleanup happens, the same conditional rendering occurs. What changes is where the logic lives. A bug fix in the cancellation logic now applies to every component that uses useFetch. A test for the fetch behavior targets the Hook, not the component.

c. The independence of custom Hook state

A common misconception is that components sharing a custom Hook also share the state inside it. They do not. Each call to a custom Hook creates completely independent state variables and effects .

Consider a useFormInput Hook that manages a single form field:

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value: value,
    onChange: handleChange,
  };
}

A form component calls this Hook twice — once for the first name, once for the last name:

function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');

  return (
    <>
      <input {...firstNameProps} />
      <input {...lastNameProps} />
      <p>
        Good morning, {firstNameProps.value} {lastNameProps.value}.
      </p>
    </>
  );
}

The two value states are entirely separate. Typing in the first input does not affect the second. The onChange handler from the first call updates only the first value; the handler from the second call updates only the second. React treats each call to useFormInput as a distinct instance with its own state slot .

This independence is what makes custom Hooks composable. A component can call the same Hook multiple times, or call different Hooks that internally call the same Hook, without any state collisions. The state lives in React’s internal storage, keyed by the order of Hook calls within the component that ultimately calls them. The custom Hook is just a function that describes a pattern of Hook calls; the state belongs to the component instance, not to the Hook function .


Complete Example Session

// ============================================
// PART 1: A COMPONENT WITH MIXED CONCERNS
// ============================================

// This component does three jobs: it manages a form field,
// it fetches data, and it subscribes to a browser event.
// The rendering logic is buried under the stateful machinery.

function UserProfile({ userId }) {
  // Job 1: form field state
  const [nickname, setNickname] = useState('');
  const handleNicknameChange = (e) => setNickname(e.target.value);

  // Job 2: data fetching
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  useEffect(() => {
    let cancelled = false;
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then((data) => {
        if (!cancelled) setUser(data);
      })
      .catch((e) => {
        if (!cancelled) setError(e);
      });
    return () => {
      cancelled = true;
    };
  }, [userId]);

  // Job 3: online status subscription
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(userId, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(userId, handleStatusChange);
    };
  }, [userId]);

  // Rendering — the actual purpose of the component
  if (error) return <ErrorBanner error={error} />;
  if (!user) return <Skeleton />;
  return (
    <div>
      <h1>{user.name}</h1>
      <StatusBadge online={isOnline} />
      <input value={nickname} onChange={handleNicknameChange} />
    </div>
  );
}


// ============================================
// PART 2: EXTRACTING THE FORM FIELD LOGIC
// ============================================

// The form field logic — state plus change handler — is reusable.
// Any input that needs controlled state can use this Hook.

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return { value, onChange: handleChange };
}


// ============================================
// PART 3: EXTRACTING THE DATA FETCHING LOGIC
// ============================================

// The fetch-loading-error pattern is the same regardless of URL.
// The Hook takes a URL and returns the result and error.

function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch(url)
      .then((r) => r.json())
      .then((value) => {
        if (!cancelled) setData(value);
      })
      .catch((e) => {
        if (!cancelled) setError(e);
      });
    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, error };
}


// ============================================
// PART 4: EXTRACTING THE SUBSCRIPTION LOGIC
// ============================================

// Subscribing to an external system and unsubscribing on cleanup
// is a self-contained pattern. The Hook hides the subscribe/
// unsubscribe ceremony behind a boolean return value.

function useOnlineStatus(friendId) {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
    };
  }, [friendId]);

  return isOnline;
}


// ============================================
// PART 5: THE COMPONENT AFTER EXTRACTION
// ============================================

// The component now calls three custom Hooks. Each call
// replaces a block of stateful machinery with a named
// function that describes what it does.

function UserProfile({ userId }) {
  const nicknameProps = useFormInput('');
  const { data: user, error } = useFetch(`/api/users/${userId}`);
  const isOnline = useOnlineStatus(userId);

  if (error) return <ErrorBanner error={error} />;
  if (!user) return <Skeleton />;
  return (
    <div>
      <h1>{user.name}</h1>
      <StatusBadge online={isOnline} />
      <input {...nicknameProps} />
    </div>
  );
}


// ============================================
// PART 6: A CUSTOM HOOK CALLING ANOTHER CUSTOM HOOK
// ============================================

// Hooks can compose. This Hook manages a chat room connection,
// and it uses useOnlineStatus internally to pause reconnection
// attempts when the user is offline.

function useChatRoom({ roomId, serverUrl }) {
  const isOnline = useOnlineStatus();
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    if (!isOnline) return;

    const connection = createConnection({ roomId, serverUrl });
    connection.on('message', (msg) => {
      setMessages((prev) => [...prev, msg]);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl, isOnline]);

  return messages;
}


// ============================================
// PART 7: USING THE COMPOSED HOOK
// ============================================

// The component does not know that useChatRoom internally
// subscribes to online status. It just gets the messages.

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const messages = useChatRoom({ roomId, serverUrl });

  return (
    <div>
      <input
        value={serverUrl}
        onChange={(e) => setServerUrl(e.target.value)}
      />
      <MessageList messages={messages} />
    </div>
  );
}


// ============================================
// PART 8: THE DATA FLOW INSIDE useChatRoom
// ============================================

// When the component renders, React calls useChatRoom.
// Inside, React calls useOnlineStatus, which sets up its
// own state and effect. Then useChatRoom runs its own effect.
// The call order is: useOnlineStatus → useState → useState →
// useEffect → useEffect. This order is the same on every render.

// If the online status changes, useOnlineStatus re-renders
// its state. useChatRoom re-runs its effect, which either
// connects or disconnects based on the new status.


// ============================================
// PART 9: WHAT THE COMPONENT SEES
// ============================================

// The ChatRoom component sees only:
//   const messages = useChatRoom({ roomId, serverUrl });
//
// It does not see:
//   - useOnlineStatus
//   - useState for messages
//   - the effect that connects and disconnects
//   - the dependency on isOnline
//
// The entire connection lifecycle is behind one function call.


// ============================================
// PART 10: THE SAME HOOKS, DIFFERENT COMPONENTS
// ============================================

// A second component can use useOnlineStatus without
// sharing state with ChatRoom. If ChatRoom is online and
// this component is online, they are online for the same
// external reason (the network), but their state variables
// are independent.

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <div>{isOnline ? 'Online' : 'Offline'}</div>;
}

// The two isOnline variables are completely separate.
// They happen to have the same value because they
// subscribe to the same external source. But typing
// in one component does not affect the other.

The ten parts show the full arc of extraction: identifying mixed concerns, pulling each concern into a named Hook, composing Hooks together, and understanding that state is independent across consumers.


Quick Reference

The Rules of Custom Hooks

RuleWhat It Means
Name starts with useRequired for React to recognize it as a Hook
Call at top levelNever inside conditions, loops, or nested functions
Call from React functionsOnly from components or other Hooks
May call other HooksThis is the purpose of custom Hooks

When to Extract a Custom Hook

ConditionExtract?
Logic used in two or more componentsYes
Logic used once but complex enough to nameYes
Logic is a single useState with no derived behaviorNo
Logic has no Hook callsNo — use a regular function
Logic produces JSXNo — use a component

Custom Hook Composition

PatternExample
Hook calls another HookuseChatRoom calls useOnlineStatus
Component calls multiple HooksUserProfile calls useFormInput and useFetch
Hook returns objectreturn { value, onChange }
Hook returns arrayreturn [value, setValue]

What a Custom Hook Returns

Return typeWhen to use
ObjectMultiple named values ({ data, error })
ArrayPositional values like useState
Single valueuseOnlineStatus returns a boolean
NothingHook only performs side effects

The Naming Test

NameVerdict
useData(url)✅ Clear purpose, clear inputs
useChatRoom(options)✅ Domain-specific, clear
useMount(fn)❌ Lifecycle wrapper, not a use case
useEffectOnce(fn)❌ Replicates useEffect behavior
useColors() returning static palette❌ Not stateful — use a constant

Best Practices

✅ Do This:

// Name the Hook after its purpose
function useFetch(url) {                                              // ✅
// Return an object for multiple values
return { data, error };                                               // ✅
// Call Hooks at the top level of the custom Hook
function useData(url) {                                               // ✅
  const [data, setData] = useState(null);                             // ✅
// Compose Hooks
function useChatRoom(options) {                                       // ✅
  const isOnline = useOnlineStatus();                                 // ✅
// Keep the Hook focused on one use case
function useOnlineStatus() {                                          // ✅
// Include all reactive values in dependencies
useEffect(() => { /* ... */ }, [url]);                                // ✅

❌ Don’t Do This:

// Don't create lifecycle wrappers
function useMount(fn) {                                               // ❌
// Don't call Hooks conditionally
if (isLoggedIn) {                                                     // ❌
  useEffect(() => { /* ... */ }, []);                                 // ❌
// Don't extract trivial wrappers
function useTitle(title) {                                            // ❌
  useState(title);                                                    // ❌
// Don't name Hooks after generic concerns
function useApi() { /* too abstract */ }                              // ❌
// Don't return JSX from a Hook
return <div>{data}</div>;                                             // ❌
// Don't call Hooks from regular functions
function setOnlineStatus() {                                          // ❌
  const [online, setOnline] = useState(true);                         // ❌

Common Pitfalls

PitfallWhy It HappensFix
Hook called conditionallyThinking conditionals work like regular functionsMove the condition inside the Hook or component
State shared between consumersExpecting Hook state to be globalUnderstand that each call creates independent state
Hook name doesn’t start with useForgetting the conventionRename the function
Lifecycle wrapper HookTrying to replicate class lifecycle methodsUse the React API directly; extract by purpose
Hook returns too many valuesNot designing the interfaceReturn an object with named keys
Trivial extractionExtracting for the sake of extractionOnly extract when it adds meaning
Missing dependencies in Hook’s effectNot treating Hook code as component bodyList every reactive value the effect reads
Hook used in event handlerMisunderstanding where Hooks can be calledMove the Hook call to the component body

Real-World Examples

1. useLocalStorage

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}

2. useDebounce

function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

3. useMediaQuery

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
  useEffect(() => {
    const mql = window.matchMedia(query);
    setMatches(mql.matches);
    const handler = (e) => setMatches(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);
  return matches;
}

4. usePrevious

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

5. useToggle

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

6. useInterval

function useInterval(callback, delay) {
  const savedCallback = useRef(callback);
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

7. useEventListener

function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef(handler);
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  useEffect(() => {
    const listener = (e) => savedHandler.current(e);
    element.addEventListener(eventName, listener);
    return () => element.removeEventListener(eventName, listener);
  }, [eventName, element]);
}

8. useIsMounted

function useIsMounted() {
  const ref = useRef(false);
  useEffect(() => {
    ref.current = true;
    return () => {
      ref.current = false;
    };
  }, []);
  return ref;
}

9. useCounter

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);
  const reset = () => setCount(initial);
  return { count, increment, decrement, reset };
}

10. useFormInput

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  const onChange = (e) => setValue(e.target.value);
  return { value, onChange };
}

Visual

The Custom Hook Call Order

┌──────────────────────────────────────────────────────────────┐
│  COMPONENT RENDER                                            │
│                                                              │
│  function ChatRoom({ roomId }) {                             │
│    const messages = useChatRoom({ roomId });                 │
│                                                              │
│  React calls useChatRoom:                                    │
│                                                              │
│    ┌────────────────────────────────────────────────────┐    │
│    │  useChatRoom                                       │    │
│    │                                                    │    │
│    │  1. useOnlineStatus()      ← calls useState        │    │
│    │                             calls useEffect        │    │
│    │  2. useState([])           ← messages state        │    │
│    │  3. useEffect(...)         ← connection effect     │    │
│    │                                                    │    │
│    │  Returns: messages array                           │    │
│    └────────────────────────────────────────────────────┘    │
│                                                              │
│  The call order is: useState, useEffect, useState, useEffect │
│  This order must be identical on every render.               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Independent State Across Consumers

┌──────────────────────────────────────────────────────────────┐
│  TWO COMPONENTS USING THE SAME CUSTOM HOOK                   │
│                                                              │
│  ChatRoom:                                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  isOnline = useState(true)      ← state slot #1      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  StatusBar:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  isOnline = useState(true)      ← state slot #2      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  These are COMPLETELY INDEPENDENT state variables.           │
│  They have the same value only because they subscribe        │
│  to the same external source (the network).                  │
│                                                              │
│  Updating one does not update the other.                     │
│  Custom Hooks share LOGIC, not STATE.                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Custom Hook Composition

┌──────────────────────────────────────────────────────────────┐
│  HOOK COMPOSITION                                            │
│                                                              │
│  Component                                                   │
│  └── useChatRoom()                                           │
│      ├── useOnlineStatus()                                   │
│      │   ├── useState()      ← isOnline state                │
│      │   └── useEffect()     ← subscribe/unsubscribe         │
│      ├── useState()          ← messages state                │
│      └── useEffect()         ← connect/disconnect            │
│                                                              │
│  The component sees:    useChatRoom() → messages             │
│  The Hook sees:         useOnlineStatus() → isOnline         │
│  React sees:            a flat list of Hook calls            │
│                                                              │
│  Composition is horizontal. Nothing is added to the          │
│  element tree. The call order is preserved.                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Extraction Decision

┌──────────────────────────────────────────────────────────────┐
│  WHEN TO EXTRACT A CUSTOM HOOK                               │
│                                                              │
│  Does the code call any Hooks?                               │
│  │                                                           │
│  ├── No  ──► Use a regular function                          │
│  │                                                           │
│  └── Yes ──► Is it used in more than one place?              │
│              │                                               │
│              ├── Yes ──► Extract a custom Hook               │
│              │                                               │
│              └── No  ──► Does extracting make the            │
│                          component easier to understand?     │
│                          │                                   │
│                          ├── Yes ──► Extract                 │
│                          │                                   │
│                          └── No  ──► Leave it in place       │
│                                                              │
│  Does the code produce JSX?                                  │
│  │                                                           │
│  └── Yes ──► Use a component, not a Hook                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Custom HookA JavaScript function starting with use that may call other Hooks
PurposeExtract stateful logic from components into reusable functions
State sharingCustom Hooks share logic, not state — each call is independent
NamingMust start with use — required for Rules of Hooks enforcement
Call locationTop level of components or other custom Hooks
Extraction criteriaCalls Hooks, reused or complex, no JSX output
CompositionCustom Hooks can call other custom Hooks
Lifecycle wrappersAvoid useMount, useEffectOnce, etc. — use the API directly
TestingLogic in Hooks is testable without rendering UI
MigrationHook internals can change without affecting consumers

Key takeaways:

  • Custom Hooks extract behavior, not markup. If the code produces JSX, it belongs in a component. If it manages state, effects, or subscriptions without rendering anything, it can be a Hook.
  • The use prefix is mandatory. It is the signal to React and to other developers that the function contains Hook calls and must follow the Rules of Hooks.
  • State is independent per call. Two components using the same Hook do not share state. They share the pattern that creates state.
  • Composition is horizontal. A Hook calling another Hook does not add to the element tree. It adds to the flat list of Hook calls that React tracks by order.
  • Extraction has a cost. A poorly named or trivial Hook makes code harder to understand. The abstraction must describe a meaningful unit of behavior.
  • Focus on concrete use cases. useChatRoom is better than useConnection because it names exactly what it does. useMount is worse than useEffect because it hides the real dependencies.
  • Hooks make React patterns migratable. When a better API arrives, rewriting the Hook internals fixes every consumer at once.
  • The component stays focused on rendering. After extraction, the component body shows what it renders, not how it manages state.

Remember: Custom Hooks are the mechanism React provides for sharing stateful logic without duplicating code or wrapping components in extra layers. The pattern is simple — a function starting with use that calls other Hooks — but the discipline is in the naming and the focus. A good custom Hook describes a concrete use case, hides the machinery behind a clear interface, and leaves the component free to do what components do best: render UI. The state inside each Hook call belongs to the component that calls it, and the order of Hook calls is the contract that keeps that state aligned across renders.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!