Custom Hook Design: Inputs, Outputs, and Error Boundaries

Learn to design a custom Hook with clear inputs, a predictable output shape, and a defined error behavior that works with React error boundaries.

6 min read

Good custom Hook design rests on three decisions: a clear input surface, a predictable output, and a defined behavior when something goes wrong. Get those right, and other developers can use the Hook without reading its source.

Design the inputs

Use positional arguments for one obvious input, and an options object when a Hook has several settings. Named options with defaults make optional settings explicit.

App.jsxApp.jsx
import { useState } from "react";
 
function useCounter({ start = 0, step = 1 } = {}) {
  const [count, setCount] = useState(start);
  function increment() {
    setCount((current) => current + step);
  }
  return { count, increment };
}

The destructured defaults let callers run the Hook with no arguments or override only the step. Each setting carries a name, so a call site reads like documentation instead of a list of mystery values.

Design the outputs

Return a tuple when the Hook mirrors useState, and an object when it returns several named values. Whatever the shape, keep it small and consistent.

  • A tuple works for one value plus one updater.
  • An object works when callers need names like count and increment.
  • A single value works when the Hook only exposes data.

Avoid recreating returned objects or functions on every render unless the change matters. Callers often place returned functions in dependency arrays, and a new identity forces Effects to re-run. A stable output also makes the Hook easier to test, since callers can assert on the same values across renders.

Separate render errors from Effect errors

Errors behave differently depending on where a Hook throws them.

App.jsxApp.jsx
function useRequiredUser(user) {
  if (user === null) {
    throw new Error("User is required");
  }
  return user;
}

This Hook throws during render, so an error boundary around the component catches it and can show a fallback. React runs Hooks during the component render, so a throwing Hook behaves like a throwing component. An error thrown inside an Effect or an event handler bypasses error boundaries, because that code runs after rendering, not during it.

Decide where failures surface

  • Throw during render when invalid data should fall back to a boundary.
  • Return an error value when the caller should decide what to show.
  • Handle rejected promises inside the Hook instead of leaking uncaught rejections.

There is no function-component error boundary yet. Write a small class boundary with getDerivedStateFromError, or use the react-error-boundary package so callers wrap only the parts that can fail.

Checklist before shipping a Hook

  • Name it with use and a capital letter.
  • Give inputs names and sensible defaults.
  • Return the smallest stable surface callers need.
  • Document cleanup and dependency behavior.
  • State whether failures throw during render or arrive as values.

See how to create a custom Hook for the extraction mechanics, and error boundaries explained for the fallback pattern.

Rune AI

Rune AI

Key Insights

  • Prefer named options with defaults for many inputs.
  • Return a tuple, an object, or one value, and keep it stable.
  • Errors thrown during render are caught by error boundaries.
  • Errors in Effects and event handlers bypass boundaries.
  • Decide per Hook whether failures throw or return a value.
RunePowered by Rune AI

Frequently Asked Questions

Should a custom Hook take positional arguments or an options object?

Use positional arguments for one obvious input, and an options object when a Hook has several settings. Named options with defaults make call sites read like documentation.

What return shape should a custom Hook use?

Return a tuple when it mirrors useState, an object when callers need several named values, or a single value when it only exposes data. Keep the surface small and stable.

Does an error boundary catch errors thrown in a custom Hook?

It catches errors thrown during render, which includes a Hook body. It does not catch errors in Effects or event handlers, because those run after rendering.

Conclusion

Design a custom Hook around three questions: what inputs it takes, what it returns, and where errors surface. Named inputs, a small stable output, and a clear render-versus-Effect error contract make the Hook easy to use without reading its source.