useState in React: Why Your Variable Stays the Same (and How to Fix It)

Nishi SurtiNishi Surti
6 min read

Beginner's Guide to useState

I won’t lie, it took me 4 days to thoroughly understand the useState hook. It's one of the first and most fundamental concepts in React, but it can be tricky. If you've ever felt that frustration, this post is for you. We're going to dive deep, starting from the basics.

Let’s go to our good old GOAT 🐐 friend: plain JavaScript.

The Plain JavaScript Approach

Imagine we have a simple component. We want to display a value, let's call it state.

JavaScript

export default function App() {
    let state = "No" 
    state = "Damn No!" // We re-assign it immediately

    return (
        <main>
            <h1 className="title">Do you know useState()?</h1>
            <button className="value">{state}</button>
        </main>
    )
}

Here, I declared a state variable using let. Because the code runs from top to bottom, the final value assigned is "Damn No!", and that's what shows up on the screen.

This isn't rocket science πŸš€. I agree. But now, let’s do some magic πŸͺ„. What if we want to change the variable's value only when the user clicks the button?

In plain JavaScript, you'd add an onClick event listener and call a function. Let's try that.

JavaScript

export default function App() {
  let state = "No";

  function handleClick() {
    state = "Damn No!";
    console.log("Function called! New state:", state); // Checking our work
  }

  return (
    <main>
      <h1 className="title">Do you know useState()?</h1>
      <button className="value" onClick={handleClick}>
        {state}
      </button>
    </main>
  );
}

When you run this and click the button, something surprising happens. If you check your developer console, you'll see "Function called! New state: Damn No!". The variable is changing. But the text on the button in the UI remains "No". 🧐

This brings us to our first golden rule.

Golden Rule #1 πŸ‘πŸ½: Simply changing the value of a local variable will not make React "react." It won’t trigger React to re-run the component and update what you see on the screen (the DOM).

We have to use the method React gives us. React is the boss here!

Enter useState

Here comes the useState() function (I’m keeping it simple, as the term β€œhook” scared 😰 me at first). Since React provides it, we need to import it.

JavaScript

import { useState } from "react";

Side Note: What's with the curly braces {}? This is called a "named import." It means we are importing a specific piece of code that the "react" library has explicitly exported with that name. If you were to peek inside React's code (e.g., in /node_modules/react/), you'd find many export statements. useState is just one of them. We use curly braces to grab it by name.

Now, let's see what this useState function gives us.

JavaScript

import { useState } from "react";

export default function App() {
  const result = useState();
  console.log(result); // Let's inspect this

  return (
    <main>
      <h1 className="title">Do you know useState()?</h1>
      <button className="value">Click Me</button>
    </main>
  );
}

If you check the console, you’ll see it logs an array with two items: [undefined, Ζ’()]. The first item is our state value (it's undefined because we didn't give it a starting value), and the second is a function.

This leads to our second rule.

Golden Rule #2 πŸ‘πŸ½: useState() always returns an array containing exactly two things:

  1. The current state value.

  2. A function to update that state value.

Let's give our state an initial value. The useState function takes one argument: the initialState.

JavaScript

const result = useState("Yes");
console.log(result); // Will now log: ["Yes", Ζ’()]

Great! We know how to set an initial state. Now, you might be tempted to do this to display it:

JavaScript

import { useState } from "react";

export default function App() {
  let result = useState("Yes");

  function handleClick() {
    result[0] = "Heck Yes!"; // Let's try to change it directly
    console.log("Clicked! New result:", result);
  }

  return (
    <main>
      <h1 className="title">Is state important to know?</h1>
      <button className="value" onClick={handleClick}>
        {result[0]} 
      </button>
    </main>
  );
}

You click the button, and... nothing changes on the screen. Why? You forgot Golden Rule #1! We are still trying to change a value directly without telling React it needs to update the UI.

I know you might be pulling your hair out, but wait! Remember that second item useState gives us? The function? That's the key.

Using State Correctly: Destructuring and the Setter Function

The standard way to work with the useState array is to "destructure" it into two separate variables. It's cleaner and easier to read.

JavaScript

// Instead of: const result = useState("Yes");
// We do this:
const [result, setResult] = useState("Yes");
  • result: This is now our state variable. Its initial value is "Yes".

  • setResult: This is the special function we must use to update result.

The convention is to name the function set followed by the variable name (e.g., [name, setName], [isLoggedIn, setIsLoggedIn]), but you could technically call it anything. Sticking to convention makes you a better, more readable developer.

Now, let's put setResult to work. But be careful! A common mistake is to call the setter function directly in your JSX:

JavaScript

// 🚨 DON'T DO THIS 🚨
return (
    <main>
      <h1 className="title">Is state important to know?</h1>
      {/* This causes an infinite loop! */}
      <button className="value">{setResult("Heck Yes!")}</button>
    </main>
);

This is a classic "too much React" error. Calling setResult tells React to re-render the component. But since setResult is being called during the render, it immediately triggers another re-render, which calls setResult again, and so on, creating an infinite loop that will crash your app.

This brings us to our final rule.

Golden Rule #3 πŸ‘πŸ½: You should only call the state setter function in response to an event, like a user interaction (e.g., onClick, onChange) or after an asynchronous task completes.

Putting It All Together: The Final, Working Code

Let's call our setResult function inside our handleClick handler. This is the correct way.

JavaScript

import { useState } from "react";

export default function App() {
  // 1. Initialize state with a value and a setter function
  const [result, setResult] = useState("Yes");

  // 2. Create a function to handle the user's click
  function handleClick() {
    // 3. Use the setter function to update the state
    setResult("Heck Yes!");
  }

  return (
    <main>
      <h1 className="title">Is state important to know?</h1>
      {/* 4. Call the handler on click and display the current state */}
      <button className="value" onClick={handleClick}>
        {result}
      </button>
    </main>
  );
}

Now, when you click the button, handleClick is called. Inside handleClick, setResult("Heck Yes!") tells React two things:

  1. Change the value of result to "Heck Yes!".

  2. Re-render the App component so the user can see the change.

πŸŽ‰πŸŽ‰ You Did It! πŸŽ‰πŸŽ‰

You've now learned the core loop of interactivity in React: a component renders with initial state, a user interaction triggers an event handler, that handler uses a setter function to update the state, and React re-renders the component with the new state. Welcome to the world of React state!

0
Subscribe to my newsletter

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

Written by

Nishi Surti
Nishi Surti

Just a common developer like you ! Let's learn together and lift up each other.