Custom Hooks share logic
When several components repeat the same state, Effect, or subscription logic, move that logic into a function whose name starts with use. React treats that naming convention as a signal that the function may call Hooks.
useOnlineStatus hides browser event subscription details behind one meaningful value.import { useEffect, useState } from "react";
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
window.addEventListener("online", updateStatus);
window.addEventListener("offline", updateStatus);
return () => {
window.removeEventListener("online", updateStatus);
window.removeEventListener("offline", updateStatus);
};
}, []);
return isOnline;
}
function SaveButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline}>
{isOnline ? "Save" : "Reconnect to save"}
</button>
);
}
A custom Hook shares logic, not one shared state value. Two components that call useOnlineStatus each get an independent state cell and Effect lifecycle, even though they reuse the same function.
Reusable stateful logic
Stateful logic is the behavior around state, such as initializing it, updating it, subscribing to an external source, and cleaning up. A custom Hook lets components reuse that behavior without forcing them to reuse the same markup.
The Hook owns behavior
It can manage state, Effects, refs, reducers, and event-oriented helper functions.
The component owns UI
It decides what JSX to render and how returned values appear to the user.
import { useState } from "react";
function useFormField(initialValue = "") {
const [value, setValue] = useState(initialValue);
function onChange(event) {
setValue(event.target.value);
}
function reset() {
setValue(initialValue);
}
return { value, onChange, reset };
}
function ProfileForm() {
const name = useFormField("Ada");
const city = useFormField();
function handleReset() {
name.reset();
city.reset();
}
return (
<form>
<input aria-label="Name" value={name.value} onChange={name.onChange} />
<input aria-label="City" value={city.value} onChange={city.onChange} />
<button type="button" onClick={handleReset}>Reset</button>
</form>
);
}
- Extract a concept, not every repeated line.
A useful custom Hook describes a purpose such as online status, form fields, media queries, or chat connections.
- Keep pure calculations as regular functions.
If a function does not call Hooks, it does not need a
useprefix. A normal function is simpler and may be called anywhere. - Keep ownership explicit.
State belongs to each component call. To share the same changing value, lift state or place it in context, then let a Hook read that context.
Rules of Hooks
Custom Hooks follow the same rules as components. These rules let React match each Hook call to the correct state, ref, or Effect on every render.
- Call Hooks at the top level.
Do not call them inside loops, conditions, nested functions, event handlers, or
try/catchblocks. Keep the sequence of Hook calls stable across renders. - Call Hooks only from React functions.
Call them from function components or other custom Hooks, not from ordinary JavaScript functions.
- Name custom Hooks with
use.Names such as
useOnlineStatuscommunicate that the Rules of Hooks apply and allow React tooling to check usage. - Pass conditions as data.
Call the Hook every render and put conditional behavior inside it. An
enabledoption can tell an Effect whether it should connect.
// Incorrect: the Hook call moves in and out of the render sequence.
if (roomId) {
useChatRoom({ roomId });
}
// Correct: the call order stays stable.
useChatRoom({ roomId, enabled: Boolean(roomId) });
function useChatRoom({ roomId, enabled }) {
useEffect(() => {
if (!enabled) return;
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId, enabled]);
}
A name that starts with use is a contract. If a function never calls Hooks, prefer a regular name. If it does call Hooks, use the prefix and follow the rules.
Custom Hook API design
The Hook's parameters and return value form an API. Design it around what the caller needs to express, while keeping implementation details inside the Hook.
- Use a specific, purpose-driven name.
useOnlineStatuscommunicates more thanuseEffectWrapper. Name the user-facing capability, not the built-in Hook used internally. - Accept reactive inputs directly.
Pass values such as
roomIdandserverUrlso dependencies remain visible. Use an options object when several related inputs make positional arguments unclear. - Return the smallest useful interface.
Return values and named actions the component needs. Keep setters, refs, and subscription machinery private unless callers truly need that control.
- Keep behavior predictable.
Document whether the Hook subscribes, fetches, or performs cleanup. Prefer actions such as
resetandretryover exposing internal state transitions. - Preserve dependency semantics.
Custom Hooks that accept callbacks may need to include them in an Effect dependency list. Callers should pass stable callbacks only when stability is actually required.
function useUser(userId) {
const [attempt, setAttempt] = useState(0);
const [result, setResult] = useState({
status: "loading",
user: null
});
useEffect(() => {
let ignore = false;
setResult({ status: "loading", user: null });
fetchUser(userId)
.then(user => {
if (!ignore) {
setResult({ status: "success", user });
}
})
.catch(() => {
if (!ignore) {
setResult({ status: "error", user: null });
}
});
return () => {
ignore = true;
};
}, [userId, attempt]);
function retry() {
setAttempt(value => value + 1);
}
return { ...result, retry };
}
function UserProfile({ userId }) {
const { status, user, retry } = useUser(userId);
if (status === "loading") return <p>Loading…</p>;
if (status === "error") return <button onClick={retry}>Retry</button>;
return <h3>{user.name}</h3>;
}
The example focuses on API shape. Production data fetching usually also represents errors and may use a framework data API or cache that handles deduplication, server rendering, and race conditions.
Check your understanding
-
If two components call the same custom Hook, do they share one state value?
Show answer
No. Each call receives independent state and Effect lifecycles. The components share the Hook's logic, not its state. Shared state needs a common owner, often reached through props or context.
-
Can a custom Hook be called only when a prop is true?
Show answer
No. Call it unconditionally at the top level so Hook order stays stable. Pass the prop into the Hook and use it to control behavior inside an Effect or calculation.
-
Should a helper named
useSortedItemskeep theuseprefix if it only callsitems.toSorted()?Show answer
No. If it calls no Hooks, make it a regular function such as
sortItems. Then it can be called anywhere without implying the Rules of Hooks apply.