State in React-Managing Component Data

State in React

In the last post, you learned that props let a component receive data from its parent, but that data is read-only and can’t be changed by the component itself. So what happens when you need a component to remember and update its own data — like a counter that increases when clicked, a form field the user is typing into, or a toggle that switches on and off?

That’s exactly what state is for.

What Is State?

State is data that a component manages internally, and that can change over time as a result of user interaction, network responses, or other events. Unlike props, state is:

  • Owned by the component itself (not passed in from outside)
  • Mutable (it can change) — but only through the proper update mechanism, never directly
  • The reason a component re-renders and updates what’s shown on screen

Whenever state changes, React automatically re-renders the component to reflect the new data — this is the core mechanism that makes React apps feel interactive and “alive.”

Introducing the useState Hook

In modern React (functional components), state is managed using the useState hook. Let’s look at a simple counter example:

jsx
import { useState } from 'react'

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

export default Counter

Let’s break this down piece by piece.

1. Importing useState

jsx
import { useState } from 'react'

useState is a hook — a special function that lets functional components “hook into” React features like state and lifecycle behavior. We’ll cover hooks more broadly in a later post, but for now, just know that useState is the hook responsible for adding state to a component.

2. Declaring State

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

This single line does a lot:

  • useState(0) initializes a piece of state with a starting value of 0
  • It returns an array with exactly two items: the current value (count) and a function to update it (setCount)
  • We use array destructuring to pull those two items into clearly named variables

You can name these variables anything you like, but the convention is [value, setValue]:

jsx
const [name, setName] = useState("")
const [isOpen, setIsOpen] = useState(false)
const [items, setItems] = useState([])

3. Updating State

jsx
<button onClick={() => setCount(count + 1)}>Increment</button>

To update state, you call the setter function (setCount) — you never modify the state variable directly. Calling setCount(count + 1) tells React: “update the count state to this new value, and re-render this component.”

Why You Can’t Update State Directly

You might wonder why this doesn’t work:

jsx
count = count + 1 // ❌ This does nothing useful

The reason is that React needs to know when state changes so it can trigger a re-render and update the UI. Simply reassigning a variable doesn’t notify React of anything — it just changes a value in memory without React ever finding out. Calling the setter function (setCount) is what actually triggers React’s re-rendering process.

This is a core rule in React: always update state through its setter function, never by direct mutation.

State with Different Data Types

State isn’t limited to numbers — it can hold strings, booleans, arrays, objects, or anything else.

String State (e.g., form input)

jsx
function NameInput() {
  const [name, setName] = useState("")

  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <p>Hello, {name}</p>
    </div>
  )
}

Boolean State (e.g., toggles)

jsx
function ToggleBox() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? "Close" : "Open"}
      </button>
      {isOpen && <p>This content is now visible!</p>}
    </div>
  )
}

Array State (e.g., a to-do list)

jsx
function TodoList() {
  const [todos, setTodos] = useState([])
  const [input, setInput] = useState("")

  const addTodo = () => {
    setTodos([...todos, input])
    setInput("")
  }

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  )
}

Notice setTodos([...todos, input]) — instead of pushing directly into the existing array, we create a brand-new array using the spread operator (...todos) and add the new item to it. This leads us to one of the most important rules of working with state.

Never Mutate State Directly

Whether you’re working with arrays, objects, or any other reference type, always create a new copy rather than modifying the existing state directly.

Incorrect (mutating state directly):

jsx
todos.push(input)   // ❌ Mutates the existing array
setTodos(todos)

Correct (creating a new array):

jsx
setTodos([...todos, input])   // ✅ Creates a new array

The same principle applies to objects:

Incorrect:

jsx
user.name = "New Name"   // ❌ Mutates the existing object
setUser(user)

Correct:

jsx
setUser({ ...user, name: "New Name" })   // ✅ Creates a new object

React relies on detecting that the state reference has changed to know it needs to re-render. If you mutate the existing object or array in place, React may not detect the change at all, leading to bugs where your UI silently fails to update.

State Updates Are Asynchronous

Another common source of confusion: state updates don’t happen immediately. Consider this code:

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

const handleClick = () => {
  setCount(count + 1)
  console.log(count) // Still logs the OLD value, not the updated one
}

React batches state updates for performance reasons, and the component doesn’t re-render (and count doesn’t reflect its new value) until after the current function finishes executing. If you need to update state based on its previous value, use the functional update form instead:

jsx
setCount((prevCount) => prevCount + 1)

This is especially important when you’re calling the setter multiple times in a row or inside asynchronous code, since it guarantees you’re always working with the most up-to-date value.


Each Component Has Its Own Isolated State

If you render the same component multiple times, each instance gets its own independent state — they don’t share or interfere with each other.

jsx
function App() {
  return (
    <div>
      <Counter />
      <Counter />
      <Counter />
    </div>
  )
}

Clicking the increment button on one Counter will only affect that specific instance’s count — the other two remain unaffected. This isolation is what makes components reusable and predictable.

Common Mistakes Beginners Make

  1. Mutating state directly instead of using the setter function (todos.push() instead of setTodos([...todos, newItem])).
  2. Expecting state to update immediately after calling the setter, instead of understanding its asynchronous nature.
  3. Using the wrong initial value type — for example, initializing with useState() (undefined) when you actually need useState([]) or useState("").
  4. Forgetting to use the functional update form (prevState => ...) when the new state depends on the previous state.
  5. Declaring state inside conditionals or loops, which violates React’s Rules of Hooks (we’ll cover this rule in more detail in the hooks post).

Recap

In this post, you learned:

  • What state is and how it differs from props
  • How to declare and update state using the useState hook
  • How to work with different state data types: strings, booleans, arrays, and objects
  • Why state must never be mutated directly, and how to correctly update arrays and objects
  • Why state updates are asynchronous, and how to handle updates that depend on previous state
  • That each component instance maintains its own independent state

You now understand the two most fundamental concepts in React: props (data passed in) and state (data managed internally). In the next post, we’ll build on this by exploring how to respond to user interactions — clicks, typing, form submissions, and more — through event handling in React.

Exit mobile version