😵 The Problem
Let's say you're building a simple counter.
You want this:
Count: 0
Then the user clicks a button:
Count: 1
Click again:
Count: 2
Simple, right?
So you might try:
function Counter() {
let count = 0;
function increment() {
count++;
}
return (
<div>
<h2>{count}</h2>
<button onClick={increment}>
+1
</button>
</div>
);
}
Looks reasonable.
But click the button…
Nothing happens. 😅
The variable changes.
The UI doesn't.
And this is where React State becomes important.
💡 The Solution: State
State is data that a React component needs to remember between renders and react to when it changes.
The most common way to create state is with useState.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
Now when setCount() runs:
state changes
↓
React schedules a re-render
↓
component runs again
↓
UI reflects the new state
That's the part that makes React different from simply changing a JavaScript variable.
🧠 State Is Not Just a Variable
This distinction is important.
Normal JavaScript:
let count = 0;
count++;
console.log(count);
The variable changes.
But React doesn't automatically know that it needs to update the screen.
With state:
const [count, setCount] = useState(0);
setCount(count + 1);
You're telling React:
"This value changed. Please update the UI."
That's the real purpose of state.
🔥 State Can Be Anything
State isn't limited to numbers.
You can store strings:
const [name, setName] = useState("");
Booleans:
const [isOpen, setIsOpen] = useState(false);
Arrays:
const [todos, setTodos] = useState([]);
Objects:
const [user, setUser] = useState({
name: "John",
role: "Developer"
});
The type isn't the important part.
The important question is:
Does this value change over time and should the UI respond to that change?
If yes, state might be the right tool.
🎯 Real Example: Toggle Button
A very common UI problem:
"When I click this button, something should appear or disappear."
That's state.
import { useState } from "react";
function Profile() {
const [showDetails, setShowDetails] = useState(false);
return (
<div>
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? "Hide Details" : "Show Details"}
</button>
{showDetails && (
<p>
Frontend Developer with 5 years of experience.
</p>
)}
</div>
);
}
The state controls the UI.
showDetails = false
↓
details hidden
showDetails = true
↓
details visible
This pattern is everywhere in React:
- dropdowns
- modals
- tabs
- menus
- accordions
- dark mode
- loading states
🔥 Updating Objects in State
Here's where beginners often get into trouble.
Suppose you have:
const [user, setUser] = useState({
name: "John",
age: 25
});
Don't do this:
user.age = 26; // ❌
You're directly mutating the existing object.
Instead:
setUser({
...user,
age: 26
});
You're creating a new object with the updated value.
This matters because React state should generally be treated as immutable.
📦 Updating Arrays in State
Same idea.
Don't do:
todos.push(newTodo); // ❌
Instead:
setTodos([
...todos,
newTodo
]);
Removing an item:
setTodos(
todos.filter(todo => todo.id !== id)
);
Updating an item:
setTodos(
todos.map(todo =>
todo.id === id
? { ...todo, completed: true }
: todo
)
);
The pattern is simple:
Create a new array/object instead of directly modifying the existing one.
⚡ The Functional Update Pattern
Here's a subtle issue that becomes important in real applications.
You might write:
setCount(count + 1);
That's perfectly fine for many situations.
But when the new state depends on the previous state, the functional form is often safer:
setCount(prevCount => prevCount + 1);
For example:
function Counter() {
const [count, setCount] = useState(0);
function incrementTwice() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
return (
<button onClick={incrementTwice}>
{count}
</button>
);
}
Now each update works from the latest state value.
You'll see this pattern a lot in production React code.
🧠 State Updates Are Not Immediate
This is another common source of confusion.
setCount(count + 1);
console.log(count);
You might expect the console to show the new value immediately.
But that's not how React state works.
The state value inside the current render doesn't suddenly change halfway through your function.
React schedules an update and then renders the component again with the new state.
A useful mental model is:
Current render
↓
setState()
↓
React schedules update
↓
New render
↓
New state value
Once you understand this, a lot of "why is my state one step behind?" bugs start making sense.
💡 Real Developer Insight
Here's the state mistake I see most often:
Putting everything into state.
For example:
const [firstName, setFirstName] = useState("John");
const [lastName, setLastName] = useState("Doe");
const [fullName, setFullName] = useState("John Doe");
Do you really need fullName as separate state?
Probably not.
You can derive it:
const fullName = `${firstName} ${lastName}`;
That's called derived data.
If a value can be calculated from existing state or props, you usually don't need another state variable for it.
Otherwise you create two sources of truth.
And sooner or later:
firstName → updated
lastName → updated
fullName → forgot to update 💀
That's a bug waiting to happen.
🏗️ State Ownership Matters
This is where React architecture starts getting interesting.
Imagine:
App
├── Header
├── Sidebar
└── Dashboard
├── Search
└── ProductList
Where should the search state live?
If only Search needs it:
function Search() {
const [query, setQuery] = useState("");
}
Perfect.
But if both Search and ProductList need the query?
The state probably belongs higher up:
function Dashboard() {
const [query, setQuery] = useState("");
return (
<>
<Search
query={query}
setQuery={setQuery}
/>
<ProductList query={query} />
</>
);
}
This is called lifting state up.
The general rule:
Keep state as close as possible to the components that need it, but high enough that all required components can access it.
That's a much better approach than putting everything in global state.
⚠️ Common Developer Mistakes
1. Changing State Directly
user.name = "Alex"; // ❌
Use:
setUser({
...user,
name: "Alex"
});2. Using State for Everything
Not every variable needs state.
If changing a value doesn't require a UI update, a normal variable or another tool may be more appropriate.
3. Creating Duplicate State
Avoid storing values that can already be calculated from props or existing state.
const fullName = `${firstName} ${lastName}`;
is usually better than maintaining another fullName state.
4. Forgetting Functional Updates
If your update depends on the previous value:
setCount(prev => prev + 1);
is usually the safer pattern.
5. Putting State Too High
Just because state can live in App doesn't mean it should.
Keep local state local when possible.
It makes components easier to understand and maintain.
0 Comments