Custom Hooks: Reuse Logic Without Sharing State Accidentally
Custom Hooks: Reuse Logic Without Sharing State Accidentally
The attendance and library panels both need a small counter with increment and reset behavior. Copying that logic into each panel risks inconsistent changes, but they must still keep independent counts.
A custom Hook is a function whose name starts with use and that calls Hooks to reuse stateful logic. Each component call gets its own Hook state. Sharing the definition does not create one shared counter.
Understand
Extract a focused stateful operation.
Build
Keep separate callers’ state independent.
Check
Distinguish a Hook from a plain calculation helper.
One definition, separate state for each caller
Definition
useCounter describes reusable behavior.
Two calls
Each panel calls it in its own component.
Independent results
Changing attendance does not change the library count.
Reuse logic, not one global state container
A plain function can calculate a total without calling Hooks. A custom Hook is useful when the reused operation needs React state, refs or synchronization. Its use prefix signals that callers must follow the Rules of Hooks, calling it unconditionally at the top level of a component or another Hook.
useCounter below returns an object containing a value and two operations. The return interface is ordinary JavaScript; React does not require a particular object shape. The function calls useState in each caller’s component, so the attendance and library panels receive independent state.
Use one counter Hook in two panels
Both panels begin at zero. Each button is named by its panel so the independence can be tested. Clicking Add attendance increments only the attendance value. No module-level mutable count is shared.
React · JavaScript with JSX
import { useState } from 'react';
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
return { count, increment: () => setCount(value => value + 1), reset: () => setCount(initial) };
}
function Panel({ name }) {
const { count, increment, reset } = useCounter();
return <section><p>{name}: {count}</p><button onClick={increment}>Add {name}</button><button onClick={reset}>Reset {name}</button></section>;
}
export default function App() {
return <main><h1>Independent counters</h1><Panel name="attendance" /><Panel name="library" /></main>;
}Expected screen and behavior
Add attendance changes attendance to 1; library remains 0. Each reset affects its own panel.Follow the Hook into each component’s state
React · Use one counter Hook in two panels
import { useState } from 'react';
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
return { count, increment: () => setCount(value => value + 1), reset: () => setCount(initial) };
}
function Panel({ name }) {
const { count, increment, reset } = useCounter();
return <section><p>{name}: {count}</p><button onClick={increment}>Add {name}</button><button onClick={reset}>Reset {name}</button></section>;
}
export default function App() {
return <main><h1>Independent counters</h1><Panel name="attendance" /><Panel name="library" /></main>;
}Rendering the attendance Panel calls useCounter and establishes its useState call. Rendering the library Panel does the same for a different component instance. The reusable source is shared; the state belongs to different positions in the React tree.
The increment function uses a functional updater, so it computes the next value from the pending count. Returning a callback does not run it during rendering. The button later calls the returned operation as an event handler.
Give the Hook a clear responsibility
A Hook named useCounter says what it manages. A vague useEverything that fetches students, updates titles and submits attendance couples unrelated responsibilities. Extract logic when the name and interface help explain a coherent operation.
Do not turn every helper into a Hook. An average calculation is a plain function. If several components truly need the same shared state, lift it to an owner or use an appropriate context/store; merely calling the same custom Hook in several places does not share one state value.
Try the school task
React · starting example
import { useState } from 'react';
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
return { count, increment: () => setCount(value => value + 1), reset: () => setCount(initial) };
}
function Panel({ name }) {
const { count, increment, reset } = useCounter();
return <section><p>{name}: {count}</p><button onClick={increment}>Add {name}</button><button onClick={reset}>Reset {name}</button></section>;
}
export default function App() {
return <main><h1>Independent counters</h1><Panel name="attendance" /><Panel name="library" /></main>;
}Create a reusable toggle Hook and use it to show or hide a school note. Expose the current flag and a toggle operation.
Worked answer
The Hook owns the boolean and updates from its current value. App uses the returned flag to describe visibility. This custom Hook wraps state behavior; the note content stays in the component.
React · JavaScript with JSX
import { useState } from 'react';
function useToggle(initial = false) {
const [open, setOpen] = useState(initial);
return [open, () => setOpen(value => !value)];
}
export default function App() {
const [open, toggle] = useToggle();
return <main><h1>School note</h1><button onClick={toggle}>Toggle note</button>
<p>{open ? 'Bring your library book' : 'Note hidden'}</p></main>;
}Expected screen and behavior
Toggle note changes Note hidden to Bring your library book, and a second click hides it.Mistakes and boundaries
Hook names do not waive Hook rules. Calling useCounter inside a click handler or conditional branch is invalid just as calling useState there is invalid.
Initial state is used when a component initializes; a changed initial argument does not automatically reset existing state. If reset should use the latest parameter, document that behavior explicitly.
Custom Hooks can encapsulate effects, but their dependencies and cleanup still matter. Hiding an Effect behind a Hook does not eliminate stale closures or unhandled request failures.
Check your reasoning
React · Use one counter Hook in two panels
import { useState } from 'react';
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
return { count, increment: () => setCount(value => value + 1), reset: () => setCount(initial) };
}
function Panel({ name }) {
const { count, increment, reset } = useCounter();
return <section><p>{name}: {count}</p><button onClick={increment}>Add {name}</button><button onClick={reset}>Reset {name}</button></section>;
}
export default function App() {
return <main><h1>Independent counters</h1><Panel name="attendance" /><Panel name="library" /></main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
Extract a clear stateful responsibility and preserve its contract. Use a shared owner when values must agree, and Hook rules in every caller.
Read the primary reference: React: reusing logic with custom Hooks.
Reaching the recap and completing the quizzes are separate milestones.