Learning path · Level 10 · Tutorial 4

Extract Reusable Stateful Logic into Custom Hooks

Extract reusable stateful behavior into a typed custom Hook while keeping rendering decisions in components. Distinguish sharing a Hook implementation from sharing its state.

  • Level: Intermediate
  • Duration: 15 min
  • 6 steps
Start tutorial Back to the path
Extract Reusable Stateful Logic into Custom Hooks

What you will go through

  1. Choose the Right Reusable Abstraction Identify which counter code is pure, stateful reusable behavior, or component presentation before extracting a custom Hook. 2 min
  2. Define a Small, Typed Hook Contract Define the typed input and return contract for a custom Hook, then apply the rules for calling it from a component. 2 min
  3. Extract the Logic Without Changing Its Lifecycle Extract the inline timer behavior into a typed custom Hook while preserving its reactive input, dependencies, functional update, and cleanup lifecycle. 4 min
  4. Reuse the Hook in Two Different Interfaces Connect two differently rendered counter components to one typed custom Hook contract while preserving presentation decisions in the consumers. 3 min
  5. Prove That Reused Logic Has Independent State Predict and observe how two consumers reuse one custom Hook implementation while keeping their state and timer synchronization independent. 2 min
  6. Review and Verify the Completed Refactor Audit the completed typed custom Hook refactor, repair its lifecycle details, and verify independent consumers locally. 3 min

What you will learn

  • Identify whether reusable behavior belongs in a plain function or a custom Hook.
  • Extract a custom Hook with typed inputs, returned values, and actions.
  • Use the Hook in two components and demonstrate that their state remains independent.
  • Preserve dependency and cleanup behavior when moving an existing Effect into a Hook.

Before you start

  • Organize React Features by Responsibility
  • Update Component State from User Events
  • Synchronize External Systems with React Effects

Step 1 of 6

Choose the Right Reusable Abstraction

Identify which counter code is pure, stateful reusable behavior, or component presentation before extracting a custom Hook.

Separate behavior from presentation

Find the behavior boundary

When counter behavior appears in more than one interface, separate what React statefully does from what a component renders.

A custom Hook is a named composition of React Hooks and related behavior. It can group state, actions, and synchronization behind one reusable boundary. It is not a special mechanism that automatically creates shared state.

For this counter, the extraction candidate is the changing behavior: count state, running state, actions, and timer synchronization. The heading, buttons, and layout remain rendering decisions for a component.

Before and after extraction

A responsibility diagram separates counter state, timer behavior, and actions into a custom Hook while heading, controls, and card layout remain in a component.

Extract stateful behavior; keep presentation decisions with the component.

Tip

Use the right abstraction

Ask one boundary question: Can this code run without React state or React Hooks?

  • If yes, it may be a plain function.
  • If it calls React Hooks or coordinates stateful behavior around them, it belongs in a custom Hook.
  • If it chooses JSX, labels, controls, or layout, it belongs in the consuming component.

This keeps the Hook focused on behavior while each consumer remains free to present that behavior differently.

Classify each responsibility

A pure formatting helper

This pure helper receives a number and returns a string. It has no React state, Effect, or rendering work, so it is a plain function—not a custom Hook.

typescript
export function formatCount(count: number): string {
  return `Ticks: ${count}`;
}

formatCount(3); // 'Ticks: 3'

Counter responsibilities

Use the responsibility, rather than the file name, to select the abstraction.

A custom Hook owns reusable stateful behavior, not the interface that displays it.
Work to doBest homeWhy
Format a numeric countPlain functionPure input-to-output calculation
Track count and running stateCustom HookStateful reusable behavior
Start and clear a browser timerCustom HookTimer synchronization belongs with behavior
Choose a heading and button arrangementComponentIt is a rendering decision
Choose compact or card-like spacingComponentIt is a layout decision

Practice: choose the abstraction

Choose the correct boundary

Match each counter responsibility to its best home.

Tap an item and then its matching pair.

Step 2 of 6

Define a Small, Typed Hook Contract

Define the typed input and return contract for a custom Hook, then apply the rules for calling it from a component.

Describe what crosses the Hook boundary

A focused boundary

A Hook contract is the consumer-facing boundary of a custom Hook. For this counter, intervalMs is the numeric input. The Hook returns current values plus semantic actions—not JSX, DOM event objects, or raw state setters.

intervalMs remains a reactive input: a consumer can pass a supported value such as 1000 or 500, and the Hook receives that value for its behavior.

Diagram showing one numeric input flowing into a custom Hook and values plus actions flowing out to a consumer.

The consumer supplies an interval input and receives a small Hook contract. The component still decides how to render those results.

Tip

Keep the contract semantic. A consumer asks the counter to start, stop, or reset; it does not need to know which state setter or browser timer operation makes that happen.

Make the contract small and semantic

Name the input and returned shape

Place the logic contract in src/features/counters/hooks/useTickCounter.ts. The named export is the custom Hook; start, stop, and reset are values returned by that Hook.

This is a contract sketch only. Its implementation comes next; consumers should depend on the types and meanings below, not on internal details.

The typed Hook contract

src/features/counters/hooks/useTickCounter.ts — contract sketch

typescript
type TickCounter = {
  count: number;
  isRunning: boolean;
  start: () => void;
  stop: () => void;
  reset: () => void;
};

export function useTickCounter(
  intervalMs: number = 1000,
): TickCounter {
  throw new Error('Not implemented.');
}

The values report the current counter state. Each action expresses one allowed transition, so a consumer does not receive setCount or setIsRunning.

Action meanings

Behavior promised by the Hook contract
ActionPromised behavior
startStart the counter
stopStop without clearing the count
resetStop and set the count to 0

Practice the declaration

Complete the contract

Complete the action callback types and the Hook declaration.

typescript
type TickCounter = {
  count: number;
  isRunning: boolean;
  start: ;
  stop: ;
  reset: ;
};

export function useTickCounter(
  : number = 1000,
):  {
  throw new Error('Not implemented.');
}

Place the Hook call and action correctly

Choose the legal consumer

Which consumer uses the custom Hook legally and connects its returned action correctly?

Step 3 of 6

Extract the Logic Without Changing Its Lifecycle

Extract the inline timer behavior into a typed custom Hook while preserving its reactive input, dependencies, functional update, and cleanup lifecycle.

Identify the extraction boundary

Starting point

Start from the inline behavior

Before extraction, CounterCard owns the counter state, semantic actions, timer synchronization, and its JSX. The extraction moves only the stateful behavior into a custom Hook. The component remains responsible for its heading, status wording, controls, and layout.

This is the complete starting version to replace in src/features/counters/components/CounterCard.tsx. Its visible behavior must stay the same after extraction.

src/features/counters/components/CounterCard.tsx — inline version

tsx
import { useEffect, useState } from 'react';

type CounterCardProps = {
  intervalMs: number;
};

export function CounterCard({ intervalMs }: CounterCardProps) {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  const start = (): void => {
    setIsRunning(true);
  };

  const stop = (): void => {
    setIsRunning(false);
  };

  const reset = (): void => {
    setIsRunning(false);
    setCount(0);
  };

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setCount((current) => current + 1);
    }, intervalMs);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [isRunning, intervalMs]);

  return (
    <section>
      <h2>Counter A</h2>
      <p aria-live='polite'>{isRunning ? 'Running' : 'Stopped'}</p>
      <p>Ticks: {count}</p>
      <button type='button' onClick={start}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

Move state and synchronization, not presentation

Extracted implementation

Put behavior in the custom Hook

Create src/features/counters/hooks/useTickCounter.ts. The custom Hook owns its state and Effect, receives intervalMs as a reactive input, and returns the Hook contract. It returns values and actions—not JSX or button handlers.

A consumer can render those returned values in any interface without knowing how the timer is synchronized.

Behavior boundary and timer lifecycle

Diagram separating component presentation from Hook state and timer behavior, with a lifecycle flow for start, interval change, stop, and unmount.

The Hook keeps the timer handle created by each setup. When a running interval changes, React cleans up that current handle before the replacement setup creates another one.

src/features/counters/hooks/useTickCounter.ts

ts
import { useEffect, useState } from 'react';

type TickCounter = {
  count: number;
  isRunning: boolean;
  start: () => void;
  stop: () => void;
  reset: () => void;
};

export function useTickCounter(intervalMs: number = 1000): TickCounter {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  const start = (): void => {
    setIsRunning(true);
  };

  const stop = (): void => {
    setIsRunning(false);
  };

  const reset = (): void => {
    setIsRunning(false);
    setCount(0);
  };

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setCount((current) => current + 1);
    }, intervalMs);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [isRunning, intervalMs]);

  return { count, isRunning, start, stop, reset };
}

Tip

Preserve the reactive behavior

intervalMs stays an input to the custom Hook. Do not copy it into state: a new input such as 500 ms must cause the Effect to replace the running interval. The functional updater also remains unchanged, so the timer callback does not rely on a captured count value.

Practice the extraction details

Complete the extracted Hook

Complete the missing cleanup method, reactive dependencies, and Hook-contract action.

ts
useEffect(() => {
  if (!isRunning) {
    return;
  }

  const intervalId = window.setInterval(() => {
    setCount((current) => current + 1);
  }, intervalMs);

  return () => {
    window.(intervalId);
  };
}, [, ]);

return {
  count,
  isRunning,
  start,
  stop,
  ,
};

Verify the replacement lifecycle

Order replacement cleanup

A running consumer changes its interval from 1000 ms to 500 ms. Put the lifecycle events in order.

  1. The replacement setup creates its interval handle.
  2. React runs cleanup for the interval from the previous setup.
  3. React runs the replacement Effect setup.
  4. The consumer supplies the new interval input.

Step 4 of 6

Reuse the Hook in Two Different Interfaces

Connect two differently rendered counter components to one typed custom Hook contract while preserving presentation decisions in the consumers.

Connect the contract to presentation

Share behavior, not markup

One Hook contract, two interfaces

CounterCard and CompactCounter are consumers of the same useTickCounter Hook contract. Each consumer calls the custom Hook once at its top level, destructures its values and actions, and decides how to render them.

The custom Hook owns counter behavior. The consumers own headings, status wording, button arrangement, and other JSX choices. App composes both consumers and owns Counter A’s selected interval.

One implementation, two presentations

A behavior module connects to a full counter card and a compact counter panel, while a parent application supplies a selected interval to the full card.

Both presentation components import one behavior implementation, while their layouts remain local.

Tip

Boundary check

Keep JSX out of the custom Hook. A consumer can use the same values and actions in a card, toolbar, dialog, or another interface without changing the Hook contract.

Build Counter A’s full interface

Counter A: full card consumer

Replace src/features/counters/components/CounterCard.tsx with this complete consumer. It receives Counter A’s selected interval and callback from App, calls the custom Hook once, and retains the full-card interface.

tsx
import { useTickCounter } from '../hooks/useTickCounter';

type CounterCardProps = {
  intervalMs: 1000 | 500;
  onIntervalMsChange: (intervalMs: 1000 | 500) => void;
};

export function CounterCard({
  intervalMs,
  onIntervalMsChange,
}: CounterCardProps) {
  const { count, isRunning, start, stop, reset } = useTickCounter(intervalMs);

  return (
    <section>
      <h2>Counter A</h2>
      <label>
        Interval
        <select
          value={intervalMs}
          onChange={(event) => {
            const selectedIntervalMs = Number(event.currentTarget.value);
            if (selectedIntervalMs === 1000 || selectedIntervalMs === 500) {
              onIntervalMsChange(selectedIntervalMs);
            }
          }}
        >
          <option value={1000}>1000 ms</option>
          <option value={500}>500 ms</option>
        </select>
      </label>
      <p>{`Ticks: ${count}`}</p>
      <p aria-live='polite'>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={start}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

Read the connection

The custom Hook is called while CounterCard renders. Later, native button handlers invoke the returned start, stop, and reset actions. Do not call a custom Hook from an event handler.

Add the compact consumer and compose the app

Counter B: compact consumer

Create or replace src/features/counters/components/CompactCounter.tsx with this complete consumer. It uses the fixed supported interval of 1000 ms and deliberately presents the same Hook contract in a different layout.

tsx
import { useTickCounter } from '../hooks/useTickCounter';

export function CompactCounter() {
  const { count, isRunning, start, stop, reset } = useTickCounter(1000);

  return (
    <section>
      <h2>Counter B</h2>
      <p>Interval: 1000 ms</p>
      <p>{`Ticks: ${count}`}</p>
      <p>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={start}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

Compose the integrated application

Replace src/App.tsx with this complete composition. App owns Counter A’s selectable interval, initially 1000. Counter B has no interval prop because its consumer uses a fixed 1000 ms input. Keep the existing src/features/counters/hooks/useTickCounter.ts implementation from the previous step and retain your existing React–TypeScript entry point and tooling.

tsx
import { useState } from 'react';
import { CompactCounter } from './features/counters/components/CompactCounter';
import { CounterCard } from './features/counters/components/CounterCard';

export function App() {
  const [counterAIntervalMs, setCounterAIntervalMs] = useState<1000 | 500>(1000);

  return (
    <main>
      <h1>Tick counters</h1>
      <CounterCard
        intervalMs={counterAIntervalMs}
        onIntervalMsChange={setCounterAIntervalMs}
      />
      <CompactCounter />
    </main>
  );
}
Expected output
Tick counters

Counter A
Interval: [1000 ms]
Ticks: 0
Stopped
[Start] [Stop] [Reset]

Counter B
Interval: 1000 ms
Ticks: 0
Stopped
[Start] [Stop] [Reset]

Expected initial rendered interface after starting your local development server. The exact browser styling depends on your existing CSS.

Practice Hook consumption and action bindings

Complete the consumer connections

Complete the Hook consumption in both consumers and the individual action bindings. Each custom Hook call belongs at the component’s top level.

tsx
export function CounterCard({ intervalMs }: CounterCardProps) {
  const  = useTickCounter(intervalMs);

  return (
    <section>
      <p>{`Ticks: ${count}`}</p>
      <p>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={}>Start</button>
      <button type='button' onClick={}>Stop</button>
      <button type='button' onClick={}>Reset</button>
    </section>
  );
}

export function CompactCounter() {
  const  = useTickCounter(1000);

  return (
    <section>
      <p>{`Ticks: ${count}`}</p>
      <p>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

Identify the presentation boundary

Keep presentation decisions with consumers

Match each responsibility to the place where it belongs after this extraction.

Tap an item and then its matching pair.

Step 5 of 6

Prove That Reused Logic Has Independent State

Predict and observe how two consumers reuse one custom Hook implementation while keeping their state and timer synchronization independent.

Map state to the consumer call

One implementation, separate calls

One imported custom Hook implementation can serve many consumers, but it does not create one shared state record. Every top-level call to useTickCounter creates local count and isRunning state for that consumer and its own timer synchronization.

State ownership follows each Hook call

A reusable Hook branches into two unconnected consumer lanes, each with its own counter state, status, and timer.

Counter A and Counter B reuse one Hook implementation while owning separate state and timer lifecycles.

Separate state, actions, and inputs

Local state is created per consumer

CounterCard calls useTickCounter once for Counter A. CompactCounter calls it once for Counter B. The calls run the same Hook implementation, but they are different call sites with independent state.

Starting, stopping, or resetting one consumer acts on the values returned to that consumer. It does not reach into the other component’s Hook call.

Example

Read the boundary correctly

Same implementation, two local instances

  • Counter A receives count, isRunning, actions, and timer synchronization from its call.
  • Counter B receives its own count, isRunning, actions, and timer synchronization from its call.

An import makes behavior reusable. It does not turn Hook state into module-level storage.

If a product genuinely needs shared state, first choose a deliberate state owner. Reusing a custom Hook alone is not a substitute for that ownership decision.

Tip

Interval inputs are local too

Changing Counter A from 1000 ms to 500 ms changes Counter A’s reactive intervalMs input only. Counter B continues using its fixed 1000 ms input and keeps its own state.

Test your mental model

Check the model

Why do Counter A and Counter B keep independent state even though they import the same custom Hook?

Verify independent behavior locally

Predict, run, and report

On your own computer, use the completed app and perform this sequence:

  1. Start Counter A. Wait until its ticks visibly change, then stop it.
  2. Start Counter B and wait until its ticks visibly change.
  3. Reset Counter A.
  4. Change Counter A’s selector from 1000 ms to 500 ms.

Before running it, predict which consumer should change after each action. Then run it and report what you observed. Discuss ticks and statuses qualitatively—do not rely on exact tick totals. Include whether Counter B’s interval changed.

Write at least 180 characters (0/180).

Step 6 of 6

Review and Verify the Completed Refactor

Audit the completed typed custom Hook refactor, repair its lifecycle details, and verify independent consumers locally.

Review the completed design

Audit the completed design

A completed extraction needs two kinds of evidence. Visible checks show that each consumer responds to its own controls. Source inspection confirms the behavior that a screen cannot prove: the reactive dependencies and cleanup of the timer Effect.

Use this final audit: the counter behavior belongs in a custom Hook; its consumer-facing Hook contract exposes values and semantic actions; consumers call it at top level and retain their JSX; and each call has independent state.

Expected initial interface

An initial Tick counters interface with Counter A at 1000 ms and Counter B at a fixed 1000 ms; both show zero ticks and stopped status.

Before interaction, both consumers render their own initial state: zero ticks and a stopped status.

Refactor review checklist

Check the implementation and the interface separately before considering the refactor complete.

The interface demonstrates behavior; the Hook source demonstrates lifecycle correctness.
Review areaConfirm
AbstractionPure formatting remains a plain function; timer behavior is a custom Hook.
Hook contractThe named Hook accepts intervalMs and returns count, isRunning, start, stop, and reset.
Consumer boundaryComponents call the Hook at top level and keep their JSX, labels, and handlers.
Effect lifecycleThe Effect depends on isRunning and intervalMs and clears its own intervalId.
State ownershipCounter A and Counter B each call the Hook, so their state and timers are independent.

Repair the compact refactor

Apply the lifecycle review

Repair this compact Hook implementation. Complete the Hook boundary and timer lifecycle without adding presentation code.

Repair the Hook implementation

Complete the missing Hook name, cleanup method, and Effect dependencies.

typescript
export function (intervalMs: number = 1000): TickCounter {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  const start = (): void => {
    setIsRunning(true);
  };

  const stop = (): void => {
    setIsRunning(false);
  };

  const reset = (): void => {
    setIsRunning(false);
    setCount(0);
  };

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setCount((current) => current + 1);
    }, intervalMs);

    return () => {
      window.(intervalId);
    };
  }, [, ]);

  return { count, isRunning, start, stop, reset };
}

Install the corrected source

Use the corrected application

Replace the four application files below in your local project. Keep your existing working React–TypeScript entry point and tooling; do not replace src/main.tsx and do not add any stylesheet dependency. Then start your local development server as you normally do.

Stateful behavior

src/features/counters/hooks/useTickCounter.ts

typescript
import { useEffect, useState } from 'react';

type TickCounter = {
  count: number;
  isRunning: boolean;
  start: () => void;
  stop: () => void;
  reset: () => void;
};

export function useTickCounter(intervalMs: number = 1000): TickCounter {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  const start = (): void => {
    setIsRunning(true);
  };

  const stop = (): void => {
    setIsRunning(false);
  };

  const reset = (): void => {
    setIsRunning(false);
    setCount(0);
  };

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setCount((current) => current + 1);
    }, intervalMs);

    return () => {
      window.clearInterval(intervalId);
    };
  }, [isRunning, intervalMs]);

  return { count, isRunning, start, stop, reset };
}

Counter A consumer

src/features/counters/components/CounterCard.tsx

tsx
import { useTickCounter } from '../hooks/useTickCounter';

type CounterCardProps = {
  intervalMs: 1000 | 500;
  onIntervalMsChange: (intervalMs: 1000 | 500) => void;
};

export function CounterCard({
  intervalMs,
  onIntervalMsChange,
}: CounterCardProps) {
  const { count, isRunning, start, stop, reset } = useTickCounter(intervalMs);

  return (
    <section>
      <h2>Counter A</h2>
      <label>
        Interval
        <select
          value={intervalMs}
          onChange={(event) => {
            const selectedIntervalMs = Number(event.currentTarget.value);
            if (selectedIntervalMs === 1000 || selectedIntervalMs === 500) {
              onIntervalMsChange(selectedIntervalMs);
            }
          }}
        >
          <option value={1000}>1000 ms</option>
          <option value={500}>500 ms</option>
        </select>
      </label>
      <p>{`Ticks: ${count}`}</p>
      <p aria-live='polite'>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={start}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

Counter B consumer

src/features/counters/components/CompactCounter.tsx

tsx
import { useTickCounter } from '../hooks/useTickCounter';

export function CompactCounter() {
  const { count, isRunning, start, stop, reset } = useTickCounter(1000);

  return (
    <section>
      <h2>Counter B</h2>
      <p>Interval: 1000 ms</p>
      <p>{`Ticks: ${count}`}</p>
      <p>{isRunning ? 'Running' : 'Stopped'}</p>
      <button type='button' onClick={start}>Start</button>
      <button type='button' onClick={stop}>Stop</button>
      <button type='button' onClick={reset}>Reset</button>
    </section>
  );
}

App composition and expected result

src/App.tsx

tsx
import { useState } from 'react';
import { CompactCounter } from './features/counters/components/CompactCounter';
import { CounterCard } from './features/counters/components/CounterCard';

export function App() {
  const [counterAIntervalMs, setCounterAIntervalMs] = useState<1000 | 500>(1000);

  return (
    <main>
      <h1>Tick counters</h1>
      <CounterCard
        intervalMs={counterAIntervalMs}
        onIntervalMsChange={setCounterAIntervalMs}
      />
      <CompactCounter />
    </main>
  );
}
Expected output
Tick counters

Counter A
Interval: 1000 ms
Ticks: 0
Stopped
Start  Stop  Reset

Counter B
Interval: 1000 ms
Ticks: 0
Stopped
Start  Stop  Reset

After Start, only that consumer becomes Running and its ticks increase. Stop changes that consumer to Stopped without clearing its ticks; Reset stops it and returns it to Ticks: 0. While Counter A is running, selecting 500 ms replaces its active interval, so its ticks increase more frequently. Counter B remains independent with its fixed 1000 ms behavior.

Record your verification

Verify the completed refactor locally

On your computer, run the corrected application and report this sequence qualitatively; do not rely on exact tick totals.

  1. Start Counter A at 1000 ms and let its ticks visibly change.
  2. While Counter A is still running, select 500 ms and observe it again.
  3. Stop Counter A and note what happens to its displayed ticks.
  4. Start Counter B and let its ticks visibly change. Reset Counter A while Counter B is running.
  5. Stop Counter B, then reset it.

Also inspect useTickCounter.ts. Report the dependency array and cleanup call you found, plus how the interface showed that Counter B remained independent and retained its fixed 1000 ms behavior.

Write at least 300 characters (0/300).

Conclude the refactor

Summary

Key conclusion

A custom Hook reuses stateful behavior, not one shared state instance. Its Hook contract exposes values and semantic actions, while each consumer retains presentation decisions. State ownership follows the Hooks composed at each top-level call.

Visible controls can demonstrate independent consumer behavior. Source inspection is still necessary to verify that reactive dependencies and cleanup preserve the timer lifecycle.

  • Use a plain function for pure formatting and a custom Hook for behavior that composes React Hooks.
  • Keep JSX, labels, and control layout in each consumer.
  • Preserve intervalMs as a reactive input and clear the exact intervalId created by the Effect.
  • Counter A and Counter B share one implementation while keeping independent state and timer synchronization.

Completed

Congratulations! You completed: Extract Reusable Stateful Logic into Custom Hooks

You reviewed, repaired, and locally verified a typed custom Hook refactor.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate