useCounter

Easy

Prompt

Create a custom React hook called useCounter that manages a counter state value with functionality to increment, decrement, and reset the counter.

This is a common interview question used to test your understanding of React Hooks and state management.

Requirements

  • The hook should return an object with the following properties:
    • count: The current count value
    • increment: A function to increase the count by the step value
    • decrement: A function to decrease the count by the step value
    • reset: A function to reset the count to its initial value
    • setCount: A function to set the count to a specific value
  • The hook should accept an optional initialValue parameter (default to 0)
  • The hook should accept an optional step parameter (default to 1) that determines by how much the counter increments/decrements

Example

function CounterComponent() {
const { count, increment, decrement, reset, setCount } = useCounter(10, 2);

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
<button onClick={() => setCount(100)}>Set to 100</button>
</div>
);
}

Playground

Hint 1

Start by creating a state variable with useState inside the useCounter hook to store the counter value.

Hint 2

For the setCount function, you can simply expose the state setter function returned by useState.

Solution

Explanation

To implement the useCounter hook, we need to create a state for the counter value and functions to manipulate that state.

First, we use the useState hook to create our counter state, initializing it with the provided initialValue (or 0 if no value is provided):

const [count, setCount] = useState(initialValue);

Next, we create three functions to manipulate the counter:

  1. increment: Increases the count by the step value

    const increment = useCallback(() => {
    setCount(prevCount => prevCount + step);
    }, [step]);
  2. decrement: Decreases the count by the step value

    const decrement = useCallback(() => {
    setCount(prevCount => prevCount - step);
    }, [step]);
  3. reset: Resets the count to the initial value

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

Additionally, we directly expose the setCount function that we get from useState. This gives users of our hook the ability to set the counter to any arbitrary value as needed.

Finally, we return an object containing the current count and all our functions.

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

This pattern is very common in React applications and custom hooks, as it provides a clean way to encapsulate and reuse stateful logic.

Why prevCount?

In our increment and decrement functions, we use the updater function pattern:

setCount(prevCount => prevCount + step);

instead of:

setCount(count + step);

This is important for ensuring our counter works correctly in all scenarios. Let's examine why with a practical example:

Using Direct State Reference

Suppose our counter is at 5 and we call increment three times in succession:

function handleMultipleIncrements() {
increment(); // setCount(5 + 1)
increment(); // setCount(5 + 1)
increment(); // setCount(5 + 1)
}

If our increment function used setCount(count + step), each call would reference the same count value of 5, resulting in:

setCount(5 + 1); // setCount(6)
setCount(5 + 1); // setCount(6)
setCount(5 + 1); // setCount(6)

After one click, our counter would only be 6, not 8 as we might expect. This happens because calling the set function does not update the count state variable in the already running code.

Using Updater Function

With our updater function approach:

function handleMultipleIncrements() {
increment(); // setCount(prevCount => prevCount + 1)
increment(); // setCount(prevCount => prevCount + 1)
increment(); // setCount(prevCount => prevCount + 1)
}

React puts these updater functions in a queue. Then, during the next render, it will call them in the same order:

  1. prevCount => prevCount + 1 receives 5 as the pending state and returns 6 as the next state.
  2. prevCount => prevCount + 1 receives 6 as the pending state and returns 7 as the next state.
  3. prevCount => prevCount + 1 receives 7 as the pending state and returns 8 as the next state.

There are no other queued updates, so React will store 8 as the current state in the end.

By convention, it's common to name the pending state argument for the first letter of the state variable name, like c for count. However, we used prevCount for clarity. You may also call it c or something else that you find clearer.

This pattern ensures our state updates correctly even when multiple updates happen in quick succession, making our useCounter hook more reliable in complex interactions.

00:00