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.
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.
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
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.
Frequently Asked Questions
Should a custom Hook take positional arguments or an options object?
What return shape should a custom Hook use?
Does an error boundary catch errors thrown in a custom Hook?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.