ProgramGuru

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.

  1. Understand

    Extract a focused stateful operation.

  2. Build

    Keep separate callers’ state independent.

  3. Check

    Distinguish a Hook from a plain calculation helper.

One definition, separate state for each caller

  1. Definition

    useCounter describes reusable behavior.

  2. Two calls

    Each panel calls it in its own component.

  3. Independent results

    Changing attendance does not change the library count.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

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.

Two sibling panels call useCounter. What is shared?
Choose one answer. You can try again.

Answer and explanation

The reusable definition, with separate state for each component call.

Correct. Custom Hooks share logic rather than one state instance.

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.

    Next lesson: Data Caching and Framework Loaders: Choose an Application Data Layer →