Skip to the code
cf.

useCounter

Write useCounter(initial, options) and export it from useCounter.js. It keeps a number, like how many cups of a tea are in a cart, and returns an object: { count, increment, decrement, reset }.

initial is where the count starts, and it's 0 when you leave it out. options is an optional object with min, max, and step. Leave out min or max and there's no limit on that side. step is 1 unless you give one.

increment() adds step and decrement() takes it away, but the count never goes above max or below min: a step that would pass a limit stops at it. With max: 10 and step: 4, going up from 8 gives 10, and going up again stays at 10.

If initial is outside the limits, the count starts at the nearest limit. reset() puts the count back where it started.

Calling increment() twice in the same event adds two steps.

function Cups() {
  const { count, increment, decrement, reset } = useCounter(1, { min: 1, max: 5 });
  return (
    <div>
      <p>Cups: {count}</p>
      <button onClick={decrement}>Fewer</button>
      <button onClick={increment}>More</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

The Preview shows Demo.jsx, a cup counter that uses your hook.

What the tests check

  1. starts at 0 when there's no initial value
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
    
  2. starts at the initial value
    const { result } = renderHook(() => useCounter(3));
    expect(result.current.count).toBe(3);
    
  3. with no options, moves by 1 with no limits
    const { result } = renderHook(() => useCounter());
    act(() => result.current.decrement());
    expect(result.current.count).toBe(-1);
    act(() => result.current.increment());
    act(() => result.current.increment());
    expect(result.current.count).toBe(1);
    
  4. increment and decrement move by step
    const { result } = renderHook(() => useCounter(5, { step: 2 }));
    act(() => result.current.increment());
    expect(result.current.count).toBe(7);
    act(() => result.current.decrement());
    act(() => result.current.decrement());
    expect(result.current.count).toBe(3);
    
  5. stops at max without going past it
    const { result } = renderHook(() => useCounter(8, { max: 10, step: 4 }));
    act(() => result.current.increment());
    expect(result.current.count).toBe(10);
    act(() => result.current.increment());
    expect(result.current.count).toBe(10);
    
  6. stops at min without going below it
    const { result } = renderHook(() => useCounter(3, { min: 0, step: 2 }));
    act(() => result.current.decrement());
    expect(result.current.count).toBe(1);
    act(() => result.current.decrement());
    expect(result.current.count).toBe(0);
    act(() => result.current.decrement());
    expect(result.current.count).toBe(0);
    
  7. reset goes back to the initial value
    const { result } = renderHook(() => useCounter(2));
    act(() => result.current.increment());
    act(() => result.current.increment());
    expect(result.current.count).toBe(4);
    act(() => result.current.reset());
    expect(result.current.count).toBe(2);
    
  8. an initial value outside the limits starts, and resets, at the nearest limit
    const high = renderHook(() => useCounter(50, { min: 1, max: 10 }));
    expect(high.result.current.count).toBe(10);
    act(() => high.result.current.decrement());
    act(() => high.result.current.reset());
    expect(high.result.current.count).toBe(10);
    const low = renderHook(() => useCounter(-4, { min: 1, max: 10 }));
    expect(low.result.current.count).toBe(1);
    
  9. two increments in the same event add two steps
    const { result } = renderHook(() => useCounter());
    const { increment } = result.current;
    act(() => {
      increment();
      increment();
    });
    expect(result.current.count).toBe(2);
    
  10. works in a component, stopping at its limit
    function Cups() {
      const { count, increment, decrement } = useCounter(1, { min: 1, max: 3 });
      return (
        <div>
          <p>Cups: {count}</p>
          <button onClick={decrement}>Fewer</button>
          <button onClick={increment}>More</button>
        </div>
      );
    }
    render(<Cups />);
    const more = screen.getByRole("button", { name: "More" });
    await user.click(more);
    await user.click(more);
    await user.click(more);
    expect(screen.getByText("Cups: 3")).toBeInTheDocument();
    await user.click(screen.getByRole("button", { name: "Fewer" }));
    expect(screen.getByText("Cups: 2")).toBeInTheDocument();
    

Learn more

useCounter.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).