Understanding the useState Hook in React: A Comprehensive Guide

Hooks are special functions introduced in React v16.8 that enable you to use state and other React features in functional components (previously, these features were only available in class components).


Key Points

  1. Why Hooks?

    • Avoid using class components just for state or lifecycle methods.

    • Make code cleaner, reusable, and easier to manage.

    • Allow sharing logic between components through custom hooks.

  2. Rules of Hooks

    • Only call hooks at the top level of a component or custom hook (not inside loops, conditions, or nested functions).

    • Only call hooks from React functions (functional components or custom hooks).


Common Built-in Hooks

  1. useStateAdds state to a functional component.

     const [count, setCount] = useState(0);
    
  2. useEffect – Handles side effects (API calls, timers, subscriptions).

     useEffect(() => {
       console.log("Component mounted or updated");
     }, []);
    
  3. useContext – Accesses values from React Context without prop drilling.

  4. useRef – Accesses DOM elements or stores mutable values without causing re-renders.

  5. useReducer – Manages complex state logic using reducers.

  6. useMemo & useCallback – Optimize performance by memoizing values or functions.

  7. Custom Hooks - You can create your own hooks (functions starting with use) to reuse logic across components.


Why Hooks Are Important

  • Eliminate the need for class components.

  • Improve code readability and logic reuse.

  • Enable powerful features (state, lifecycle, refs) in functional components.


useState

  • What it is:

    useState is a React Hook that allows you to add state (data that can change) to functional components.

  • Syntax:

      const [state, setState] = useState(initialValue);
    
  • How it works:

    • When setState is called, React re-renders the component with the updated value.

    • State is preserved between renders.


Without using useState:

function App() {
    let count = 0;
    const Increment = () => {
        count += 1;
        document.querySelector('h1').innerText = `Count: ${count}`;
    }

    const Decrement = () => {
        count -= 1;
        document.querySelector('h1').innerText = `Count: ${count}`;
    }
    return (
        <div>
            <h1>Count: {count} </h1>
            <button onClick={()=>{Increment()}}>Increment</button>
            <button onClick={()=>{Decrement()}}>Decrement</button>
        </div>
    );   
}

This code is bad because:

  1. Direct DOM manipulation: Using document.querySelector and manually updating innerText bypasses React’s virtual DOM, which breaks React’s rendering flow.

  2. No reactivity: count is a local variable, so React doesn’t know it changed and won’t re-render the component.

  3. Unpredictable UI updates: If React re-renders for any reason, count resets to 0 and the manual DOM changes can get overwritten. re-renders

💡 What’s the Real Solution?

You need a way to tell React:

“Hey, something changed. Please re-render the UI with updated values.”

That’s where React’s hooks — especially useState — come in. 😉

Basic Structure of useState

const [state, setState] = useState(initialValue);
  • state: The current value of the state

  • setState: A function to update the state

  • initialValue: The starting value for the state

How It Works in Your Counter Component

const [count, setCount] = useState(0);
  1. Initialization: When the component first renders, count is set to 0 (the initial value passed to useState)

  2. State Update: When setCount is called it:

    • Updates the state value

    • Triggers a re-render of the component

    • Provides the new value during the next render

Key Characteristics of useState

  1. State Preservation: Unlike regular variables (which reset on each render), React preserves the state between renders

    What does it mean:

    In React, every time a component re-renders, all variables declared inside the function get reset, except those stored in useState.

    This is why React uses hooks like useState to preserve values between renders.


    ⚠️ Example Without useState (Regular Variable):

     function Counter() {
         let count = 0;
    
         function handleClick() {
             count++;
             console.log("Clicked:", count);
         }
    
         return <button onClick={handleClick}>Click Me</button>;
     }
    

    ❌ Problem:

    • Every time you click the button, the component re-renders.

    • The count variable resets to 0 on every render.

    • Even though count++ occurs, the count never increases consistently in the UI or in logs.


✅ Example With useState:

    function App() {
        let[count, setCount] = useState(0);

        const Increment = () => {
            count += 1;
            setCount(count);
        }

        function Decrement () {
            count -= 1;
            setCount(count);
        }
        return (
            <div>
                <h1>Count: {count} </h1>
                <button onClick={()=>{Increment()}}>Increment</button>
                <button onClick={Decrement}>Decrement</button>
            </div>
        );   
    }

✅ What Happens Now:

  • useState(0) initializes count once.

  • Even after the component re-renders, React remembers the previous count value.

  • Each time you click the button, setCount() updates the state, which causes a re-render with the new value preserved.

      Initial Render:
      count = 0 (initialized) → Display "Count is: 0"
    
      [User clicks Increment]
      setCount(0 + 1) →
      Re-render with count = 1 →
      Display "Count is: 1"
    
      [User clicks Increment again]
      setCount(1 + 1) →
      Re-render with count = 2 →
      Display "Count is: 2"
    
      [User clicks Decrement]
      setCount(2 - 1) →
      Re-render with count = 1 →
      Display "Count is: 1"
    

Why This is Better Than the Previous Approaches

  1. Automatic UI Updates: React handles DOM updates when state changes

  2. Predictable State Management: Single source of truth for your data

  3. Optimized Performance: React batches updates and only updates necessary DOM elements

  4. Cleaner Code: No manual DOM manipulation needed

  5. React's Full Power: Works seamlessly with other React features like effects, context, etc.

Common Usage Patterns

  1. Multiple State Variables:

     const [count, setCount] = useState(0);
     const [name, setName] = useState('');
    
  2. Object State:

     const [user, setUser] = useState({ name: '', age: 0 });
    
  3. Functional Initial State (for expensive calculations):

     const [data, setData] = useState(() => expensiveCalculation());
    

Best Practices

  1. Don't Modify State Directly: Always use the setter function

  2. Use Functional Updates when the new state depends on the previous state

  3. Split Complex State into multiple useState calls when possible

  4. Consider Custom Hooks for reusable state logic

useState is the foundation of state management in React functional components, enabling them to be as powerful as class components with simpler syntax and better organization.

10
Subscribe to my newsletter

Read articles from Hridoy Chowdhury directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hridoy Chowdhury
Hridoy Chowdhury

I'm a Computer Science & Engineering student exploring the world of Blockchain, Web Development (MERN), and coding interview prep. I write to reinforce my learning and share insights that make complex ideas simpler. Always building, always learning.