Skip to main content

Command Palette

Search for a command to run...

useState Hook in React [part-1]

Practical guide to understanding React's useState hook with a counter

Updated
4 min read
useState Hook in React [part-1]

State management sits at the heart of React development. The useState hook transforms static components into dynamic, interactive experiences. This guide explores building counters with useState, revealing patterns that extend far beyond simple increment buttons.

Understanding useState Fundamentals

The useState hook enables functional components to maintain and update local state. It returns an array containing the current state value and a function to update it.

It must be defined and used inside functional component only.

useState is a fundamental React Hook that lets functional components manage state (data that changes over time) by declaring a state variable, its initial value, and a function to update it, returning them as an array [stateValue, setStateFunction]. It's used for simple state like numbers, strings, objects, or arrays.

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

Here count is a variable, it’s value can be updated only using setCount function.

This elegant API masks sophisticated behavior. React tracks state changes, triggers re-renders, and ensures your UI stays synchronized with application data.

How it works

  • Import: import { useState } from 'react';.

  • Declaration: const [count, setCount] = useState(0);.

    • count: The current state value (e.g., 0).

    • setCount: The function to update count.

    • useState(0): Sets the initial value (can be any data type).

  • Updating: Call the setter function, e.g., setCount(count + 1) or setCount(prevCount => prevCount + 1). Difference b/w this is when you want to update previous value, when there is count getting updated more than one time. First one will only update once even though called multiple times.

  • Re-render: Calling the setter function tells React to re-render the component with the new state value.

A regular variable is like a whiteboard that gets erased every time you leave the room (re-render). useState is like a notebook where React writes down the value so it is waiting for you when you come back.

IMP Internally, React holds state in an array or a similar structure associated with that specific component's position in the UI tree. When it is re-render react checks the stored value if it is updated then react ignores the initial state. The state remains stored as long as the component remains in its specific position in the UI tree (it is "mounted").

IMP One important point is that when a state requires complex computation for its initial value, it should be initialized using a function, for example: useState(() => getInitialValue()). If the state is initialized directly, the computation may run on every re-render. However, when using the functional initializer, the function is executed only on the first render, which improves performance.

// Correct: The function is only called once
const [data, setData] = useState(() => performExpensiveTask());

// Incorrect: performExpensiveTask() runs on every single render
const [data, setData] = useState(performExpensiveTask());

Building a Counter

Let's build a counter that demonstrates real-world patterns you'll use across React applications.

import { useState } from 'react';

function InteractiveCounter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <div className="counter-container">
      <h2>Current Count: {count}</h2>
      <div className="button-group">
        <button onClick={decrement} disabled={count === 0}>
          Decrease
        </button>
        <button onClick={reset}>Reset</button>
        <button onClick={increment}>Increase</button>
      </div>
      <p className="counter-status">
        {count === 0 && "Start counting!"}
        {count > 0 && `You've counted ${count} times`}
        {count < 0 && "Negative territory!"}
      </p>
    </div>
  );
}

Every call to setCount schedules a re-render. React compares the new state with the previous state. If they differ, React updates the component and its children.

Real-World Counter Applications

Counters appear everywhere in production applications:

  • Inventory Management: Track product quantities with increment/decrement controls.

  • Shopping Carts: Adjust item quantities before checkout.

  • Form Inputs: Create numeric steppers for age, quantity, or rating inputs.

  • Analytics Dashboards: Display real-time counters for metrics like page views or active users.

Advanced Counter: Multi-Step Incrementer

function SmartCounter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  const adjustCount = (direction) => {
    setCount(count + (step * direction));
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <div>
        <label>
          Step Size:
          <input
            type="number"
            value={step}
            onChange={(e) => setStep(Number(e.target.value))}
            min="1"
          />
        </label>
      </div>
      <button onClick={() => adjustCount(-1)}>-{step}</button>
      <button onClick={() => adjustCount(1)}>+{step}</button>
    </div>
  );
}

This example combines multiple state values and demonstrates controlled inputs, a critical pattern for form handling.


Common Pitfalls to Avoid

Direct State Mutation

Never modify state directly:

// Wrong
count = count + 1;

// Correct
setCount(count + 1);

Direct mutation bypasses React state management, preventing re-renders and breaking your UI.

Performance Considerations

useState updates are efficient for simple values like numbers and strings. For primitive values, this comparison is instantaneous (quick ~ O(1)).

Multiple state updates in the same event handler are batched automatically in React 18+, preventing unnecessary re-renders. In same function all useState update will count to one render only.

Testing State Updates

import { render, screen, fireEvent } from '@testing-library/react';

test('increments counter', () => {
  render(<InteractiveCounter />);
  const button = screen.getByText('Increase');

  fireEvent.click(button);
  expect(screen.getByText(/Current Count: 1/)).toBeInTheDocument();
});

Testing state changes validates your component's core functionality.