useCounter
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 valueincrement
: A function to increase the count by the step valuedecrement
: A function to decrease the count by the step valuereset
: A function to reset the count to its initial valuesetCount
: 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
Start by creating a state variable with useState
inside the useCounter
hook to store the counter value.
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:
-
increment
: Increases the count by the step valueconst increment = useCallback(() => {setCount(prevCount => prevCount + step);}, [step]); -
decrement
: Decreases the count by the step valueconst decrement = useCallback(() => {setCount(prevCount => prevCount - step);}, [step]); -
reset
: Resets the count to the initial valueconst 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:
prevCount => prevCount + 1
receives 5 as the pending state and returns 6 as the next state.prevCount => prevCount + 1
receives 6 as the pending state and returns 7 as the next state.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.