Easy

useCounter

Prompt

Create a custom hook useCounter that manages a counter with increment, decrement, and reset functionality.

The hook should accept an optional initialValue (default 0) and an optional step (default 1), and return an object with:

  • count — the current value
  • increment — increases count by step
  • decrement — decreases count by step
  • reset — resets count to initialValue
  • setCount — sets count to any value

Playground

Hint 1

You need one piece of state: the count. useState(initialValue) gives you the value and a setter.

Hint 2

For increment and decrement, use the functional form of the setter: setCount(prev => prev + step). This ensures correctness even when multiple updates happen in quick succession.

Solution

Explanation

If you've used useState before, you already know everything you need for this hook. The whole idea is to take the raw useState setter and wrap it with friendlier, named functions like increment, decrement, and reset. Instead of the consumer writing setCount(prev => prev + 1) everywhere, they just call increment(). Cleaner, less room for mistakes.

export const useCounter = (initialValue = 0, step = 1) => {
const [count, setCount] = useState(initialValue);

const increment = useCallback(() => {
setCount((prev) => prev + step);
}, [step]);

const decrement = useCallback(() => {
setCount((prev) => prev - step);
}, [step]);

const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);

return { count, increment, decrement, reset, setCount };
};

We wrap each function in useCallback so that the function reference stays the same between renders. If you're passing these functions as props to child components, a stable reference prevents unnecessary re-renders. The dependency arrays ([step] and [initialValue]) make sure the functions get recreated only when those values actually change.

One thing that trips people up

You might wonder why we write setCount(prev => prev + step) instead of the simpler setCount(count + step). Here's the problem with the simpler version:

Imagine count is 5 and someone calls increment() three times quickly. With setCount(count + step), all three calls see count as 5 because React hasn't re-rendered yet. So all three calls do setCount(6). You end up at 6 instead of 8. That's a bug.

With setCount(prev => prev + step), React queues each updater and chains them: 5 → 6 → 7 → 8. Each call gets the result of the one before it. You end up at 8, which is correct.

This is a really common interview gotcha. Whenever your next state depends on your previous state, always use the updater function form. It's safer and it's what React was designed for.