What is useState in React?
useState is a React Hook used to add state (data that can change over time) to a functional component.
A component normally executes from top to bottom when React renders it. If you store changing data in an ordinary variable, changing that variable does not tell React to render the component again.
useState solves this problem.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Count: {count}</h3>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Here:
const [count, setCount] = useState(0);
creates two related things:
count→ current state valuesetCount→ function used to update that state0→ initial state value
When setCount() updates the state, React schedules a new render, and the component receives the updated count.
The important concept is:
User Action
↓
setState()
↓
State changes
↓
React schedules re-render
↓
Component executes again
↓
UI displays new state
Why Do We Need useState?
Consider this:
function Counter() {
let count = 0;
const increment = () => {
count++;
console.log(count);
};
return (
<div>
<h3>{count}</h3>
<button onClick={increment}>Increment</button>
</div>
);
}
The variable count changes:
0 → 1 → 2 → 3
but the UI does not update.
Why?
Because React doesn't know that count changed.
An ordinary JavaScript variable has no relationship with React's rendering mechanism.
With useState:
const [count, setCount] = useState(0);
React knows that count is state.
When you do:
setCount(1);
React schedules another render.
Therefore:
Normal variable
count = 0
↓
count = 1
↓
React doesn't know
↓
No UI update
Whereas:
useState
count = 0
↓
setCount(1)
↓
React knows state changed
↓
Re-render
↓
UI shows 1
Basic Syntax of useState
The general syntax is:
const [state, setState] = useState(initialValue);
For example:
const [name, setName] = useState("Syed");
You can think of it as:
state → current value
setState → function to request an update
initialValue → starting value
Another example:
const [age, setAge] = useState(48);
And:
const [isActive, setIsActive] = useState(true);
And:
const [items, setItems] = useState([]);
useState Can Store Different Types
useState isn't limited to numbers.
It can store almost any JavaScript value.
String State
const [name, setName] = useState("Syed");
Update:
setName("John");
Number State
const [age, setAge] = useState(48);
Update:
setAge(49);
Boolean State
const [isVisible, setIsVisible] = useState(true);
Update:
setIsVisible(false);
Array State
const [users, setUsers] = useState<string[]>([]);
Update:
setUsers(["Syed", "John", "David"]);
Object State
const [user, setUser] = useState({
id: 1,
name: "Syed",
age: 48
});
Update:
setUser({
id: 1,
name: "John",
age: 30
});
Complex State
State can contain nested objects and arrays:
const [user, setUser] = useState({
id: 1,
name: "Syed",
address: {
city: "Chennai",
country: "India"
},
skills: ["C#", ".NET", "React"]
});
So the type of state depends on your application requirements.
Using Multiple useState Variables
A component can have multiple state variables.
function UserProfile() {
const [name, setName] = useState("Syed");
const [age, setAge] = useState(48);
const [isActive, setIsActive] = useState(true);
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
<p>Status: {isActive ? "Active" : "Inactive"}</p>
</div>
);
}
Each state is independent:
name
↓
setName()
age
↓
setAge()
isActive
↓
setIsActive()
Updating name does not automatically change age or isActive.
For example:
setName("John");
only updates the name state.
Updating State
The setter returned by useState is used to update state.
const [count, setCount] = useState(0);
setCount(10);
After the update, React renders the component again and:
count
becomes:
10
You should generally not assign directly to the state variable:
count = 10; // Incorrect
Instead:
setCount(10);
The setter is the mechanism that tells React that the state needs to be updated.
Updating State from an Event
A very common use of useState is responding to user events.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>
+
</button>
<button onClick={() => setCount(count - 1)}>
-
</button>
</div>
);
}
The flow is:
Initial render
count = 0
Click +
↓
setCount(1)
↓
React re-renders
↓
count = 1
Click +
↓
setCount(2)
↓
React re-renders
↓
count = 2
Important: State Updates Are Not Immediately Available
Consider:
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
console.log(count);
};
You might expect:
1
but the console can show:
0
because calling setCount() requests a state update; it does not immediately replace the current count value inside the currently executing render/event handler.
Think of each render as having its own snapshot:
Render 1
count = 0
↓
click
↓
setCount(1)
↓
current handler still sees count = 0
↓
React performs next render
↓
Render 2
count = 1
This concept becomes especially important when performing multiple updates.
Updating State Multiple Times
This code can produce an unexpected result:
const increment = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
You might expect:
0 → 3
but the result is generally:
0 → 1
Why?
All three expressions are based on the same count value from the current render.
If:
count === 0
then all three effectively request:
setCount(1);
setCount(1);
setCount(1);
Functional State Update
When the next state depends on the previous state, use the functional updater form.
setCount(previousCount => previousCount + 1);
Now you can safely do:
const increment = () => {
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
};
The updates are processed using the previous state:
0
↓
previousCount = 0 → 1
↓
previousCount = 1 → 2
↓
previousCount = 2 → 3
Result:
3
This is one of the most important patterns when using useState.
Direct Update vs Functional Update
Direct update
setCount(count + 1);
Use this when the new value can be calculated directly from the state value you already have.
Functional update
setCount(previousCount => previousCount + 1);
Use this when the new value depends on the previous state, especially when there may be multiple updates.
For example:
setCount(prev => prev + 1);
is preferable when incrementing/decrementing state.
useState with Objects
A very common requirement is storing an object.
const [user, setUser] = useState({
name: "Syed",
age: 48,
city: "Chennai"
});
Suppose you want to change only the name.
A common mistake is:
setUser({
name: "John"
});
This replaces the entire object.
The resulting state becomes conceptually:
{
name: "John"
}
The previous:
age
city
are no longer present.
Instead, use the spread operator:
setUser(previousUser => ({
...previousUser,
name: "John"
}));
Now the result is:
{
name: "John",
age: 48,
city: "Chennai"
}
The important principle is:
Existing object
+
new property value
↓
New object
↓
React receives new state
Updating Multiple Object Properties
setUser(previousUser => ({
...previousUser,
name: "John",
age: 30
}));
The existing properties are preserved while the specified properties are replaced.
Nested Object State
Suppose:
const [user, setUser] = useState({
name: "Syed",
address: {
city: "Chennai",
country: "India"
}
});
To change the city:
setUser(previousUser => ({
...previousUser,
address: {
...previousUser.address,
city: "Bangalore"
}
}));
The important idea is that each nested level that you change needs to be copied appropriately.
The resulting state becomes:
{
name: "Syed",
address: {
city: "Bangalore",
country: "India"
}
}
useState with Arrays
State can contain an array.
const [users, setUsers] = useState<string[]>([
"Syed",
"John",
"David"
]);
You should not directly modify the existing state array.
Avoid:
users.push("Ahmed");
Instead create a new array:
setUsers(previousUsers => [
...previousUsers,
"Ahmed"
]);
Result:
Before:
["Syed", "John", "David"]
After:
["Syed", "John", "David", "Ahmed"]
This follows React's immutable update pattern.
Adding Items to an Array
const addUser = () => {
setUsers(previousUsers => [
...previousUsers,
"Ahmed"
]);
};
The existing users remain and the new user is appended.
Removing Items from an Array
Use filter():
const removeUser = (name: string) => {
setUsers(previousUsers =>
previousUsers.filter(user => user !== name)
);
};
For example:
Before:
["Syed", "John", "David"]
Remove John
After:
["Syed", "David"]
Updating an Array Item
Use map() when you want to replace or modify an item.
const [users, setUsers] = useState([
{ id: 1, name: "Syed", active: true },
{ id: 2, name: "John", active: false }
]);
To activate John:
setUsers(previousUsers =>
previousUsers.map(user =>
user.id === 2
? { ...user, active: true }
: user
)
);
The result:
[
{ id: 1, name: "Syed", active: true },
{ id: 2, name: "John", active: true }
]
This pattern is extremely common in React applications.
Boolean State
Boolean state is commonly used for UI behavior.
const [isVisible, setIsVisible] = useState(false);
Toggle it:
setIsVisible(previous => !previous);
This is better than:
setIsVisible(!isVisible);
when you specifically want to derive the next value from the previous value.
Example:
function Panel() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(prev => !prev)}>
Toggle
</button>
{isVisible && (
<div>
This panel is visible.
</div>
)}
</div>
);
}
Here the same state controls whether the UI is rendered.
isVisible = false
↓
Panel hidden
Click
↓
isVisible = true
↓
Panel displayed
Click
↓
isVisible = false
↓
Panel hidden
useState for Form Inputs
One of the most common uses of useState is managing form fields.
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<form>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">
Login
</button>
</form>
);
}
This is called a controlled input.
The flow is:
User types
↓
onChange
↓
setEmail()
↓
State changes
↓
React renders
↓
value receives updated state
For example:
User types "s"
↓
email = "s"
User types "y"
↓
email = "sy"
User types "e"
↓
email = "sye"
User types "d"
↓
email = "syed"
Managing Multiple Form Fields with One Object
Instead of having:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
you can store the entire form in one object:
const [form, setForm] = useState({
name: "",
email: "",
phone: ""
});
Then update a field dynamically:
const handleChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const { name, value } = e.target;
setForm(previousForm => ({
...previousForm,
[name]: value
}));
};
Then:
<input
name="name"
value={form.name}
onChange={handleChange}
/>
<input
name="email"
value={form.email}
onChange={handleChange}
/>
<input
name="phone"
value={form.phone}
onChange={handleChange}
/>
The important JavaScript feature here is:
[name]: value
which is a computed property name.
If:
name = "email"
value = "syed@example.com"
then:
[name]: value
becomes:
email: "syed@example.com"
This approach is useful for larger forms.
useState with null
Sometimes state does not have a value initially.
Use null when that represents the application's meaning:
const [selectedUser, setSelectedUser] =
useState<User | null>(null);
Initially:
selectedUser === null
After selecting a user:
setSelectedUser({
id: 1,
name: "Syed"
});
Now the state contains a user.
This pattern is particularly useful for:
- selected records
- logged-in users
- modal data
- API results
- optional values
useState with TypeScript
TypeScript can often infer the state type.
const [name, setName] = useState("Syed");
TypeScript understands:
name → string
Similarly:
const [count, setCount] = useState(0);
TypeScript infers:
count → number
You can also explicitly specify the type:
const [count, setCount] = useState<number>(0);
Both are valid.
TypeScript State with Arrays
const [users, setUsers] = useState<string[]>([]);
Now TypeScript knows that the array contains strings.
Therefore:
setUsers(["Syed", "John"]);
is valid.
But:
setUsers([10, 20]);
produces a TypeScript error because the state expects string[].
For objects:
type User = {
id: number;
name: string;
active: boolean;
};
const [users, setUsers] = useState<User[]>([]);
Now the state is strongly typed.
useState with Union Types
Suppose a state can contain either a string or null:
const [message, setMessage] = useState<string | null>(null);
Initially:
message = null
Later:
setMessage("Login successful");
Both values are valid because the type is:
string | null
Another example:
const [status, setStatus] =
useState<"loading" | "success" | "error">("loading");
Now only these values are allowed:
setStatus("loading");
setStatus("success");
setStatus("error");
This is useful for representing application states.
Lazy Initialization
Sometimes calculating the initial state is expensive.
For example:
const [users, setUsers] = useState(loadUsers());
Be careful: the expression passed to useState can be evaluated during component execution.
For expensive initialization, React supports passing an initializer function:
const [users, setUsers] = useState(() => loadUsers());
The function tells React how to calculate the initial state.
For example:
function loadUsers() {
console.log("Loading users...");
return [
{ id: 1, name: "Syed" },
{ id: 2, name: "John" }
];
}
const [users, setUsers] = useState(() => loadUsers());
The important distinction is:
useState(loadUsers())
means the function call is made as part of evaluating the component.
Whereas:
useState(() => loadUsers())
provides an initializer function for React to use for the initial state.
This pattern is useful when initial state calculation is expensive.
useState and Previous State
Whenever your next state depends on the previous state, use the callback form:
setState(previousState => newState);
Examples:
Counter
setCount(prev => prev + 1);
Toggle
setIsOpen(prev => !prev);
Add array item
setUsers(prev => [
...prev,
newUser
]);
Remove array item
setUsers(prev =>
prev.filter(user => user.id !== userId)
);
Update object
setUser(prev => ({
...prev,
name: "John"
}));
This pattern is one of the most important useState patterns to remember.
useState Does Not Merge Objects Automatically
This is an important difference for developers coming from class-based React.
Suppose:
const [user, setUser] = useState({
name: "Syed",
age: 48
});
If you do:
setUser({
name: "John"
});
React does not automatically preserve:
age
The object state is replaced.
Therefore, use:
setUser(prev => ({
...prev,
name: "John"
}));
to preserve the other properties.
useState Does Not Deep Merge Objects
Even when using:
setUser(prev => ({
...prev,
address: {
city: "Chennai"
}
}));
the spread:
...prev
only copies the top-level properties.
It does not automatically deep-merge nested objects.
For example, if the existing state is:
{
name: "Syed",
address: {
city: "Chennai",
country: "India"
}
}
then replacing address with:
address: {
city: "Bangalore"
}
removes the previous country property unless you also copy the nested object:
setUser(prev => ({
...prev,
address: {
...prev.address,
city: "Bangalore"
}
}));
State and Rendering
A state change can cause the component to render again.
For example:
function Counter() {
const [count, setCount] = useState(0);
console.log("Component rendered");
return (
<button onClick={() => setCount(prev => prev + 1)}>
{count}
</button>
);
}
Initially:
Component rendered
count = 0
After clicking:
setCount()
↓
React schedules update
↓
Component renders again
↓
count = 1
The component function can therefore execute multiple times during its lifetime.
This is why you should not think of:
const [count, setCount] = useState(0);
as creating a normal variable that simply stays in memory during the function execution.
React maintains the state across renders.
State Persists Across Renders
Consider:
function Counter() {
const [count, setCount] = useState(0);
console.log(count);
return (
<button onClick={() => setCount(prev => prev + 1)}>
{count}
</button>
);
}
Even though the component function executes again, the state doesn't go back to:
0
after every render.
Instead:
First render
count = 0
Click
↓
setCount(1)
Second render
count = 1
Click
↓
setCount(2)
Third render
count = 2
React preserves the state between renders.
useState and Component Re-rendering
Changing state does not mean React destroys and recreates the component from scratch in the conceptual sense.
Instead, React performs another render and reconciles the resulting UI with the previous UI.
For example:
const [count, setCount] = useState(0);
When:
setCount(1);
React renders the component again:
count === 1
and determines what UI needs to change.
This is one reason state is central to interactive React applications.
Updating State During Rendering
You generally should not call a state setter unconditionally during rendering.
Avoid:
function Component() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
This creates a render/update loop.
The normal pattern is to update state in response to:
- user events
- effects
- asynchronous operations
- other controlled application events
For example:
<button onClick={() => setCount(prev => prev + 1)}>
Increment
</button>
useState with Conditional Rendering
State is often used together with conditional rendering.
function Login() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
{isLoggedIn ? (
<h3>Welcome back!</h3>
) : (
<h3>Please login</h3>
)}
<button onClick={() => setIsLoggedIn(prev => !prev)}>
Toggle Login
</button>
</div>
);
}
Here state determines which part of the UI is rendered.
isLoggedIn = false
↓
Please login
isLoggedIn = true
↓
Welcome back!
useState with Loading State
A very common API scenario is:
const [loading, setLoading] = useState(false);
Then:
const loadData = async () => {
setLoading(true);
try {
// API call
} finally {
setLoading(false);
}
};
The UI can respond to the state:
{loading ? (
<p>Loading...</p>
) : (
<p>Data loaded</p>
)}
This gives you a simple state machine:
loading = false
↓
User clicks Load
↓
loading = true
↓
Show loading indicator
↓
API completes
↓
loading = false
↓
Show data
Multiple States for API Calls
A more complete example:
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<string | null>(null);
Now the component can represent:
Loading
Success
Error
For example:
const loadUsers = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
} catch {
setError("Unable to load users");
} finally {
setLoading(false);
}
};
The states represent different aspects of the request.
useState and Derived Values
Not everything needs to be state.
Suppose:
const [firstName, setFirstName] = useState("Syed");
const [lastName, setLastName] = useState("Ahmed");
You could calculate:
const fullName = `${firstName} ${lastName}`;
There is usually no need for:
const [fullName, setFullName] = useState("");
because fullName is derived from existing state.
A useful rule is:
If a value can be calculated from existing props/state during rendering, it usually does not need its own state.
For example:
const [users, setUsers] = useState<User[]>([]);
const activeUsers = users.filter(user => user.active);
activeUsers is derived data.
Don't unnecessarily create:
const [activeUsers, setActiveUsers] = useState<User[]>([]);
unless there is a specific reason.
useState vs Normal Variable
| Feature | Normal variable | useState |
|---|---|---|
| Stores a value | Yes | Yes |
| Persists across renders | No | Yes |
| Updating it causes re-render | No | Yes |
| React tracks it | No | Yes |
| Useful for UI state | Limited | Yes |
| Setter function | No | Yes |
Example:
let count = 0;
versus:
const [count, setCount] = useState(0);
For React UI state, useState is generally the appropriate mechanism.
useState vs useRef
Both can preserve values between renders, but they serve different purposes.
const [count, setCount] = useState(0);
Changing it causes a render:
setCount(10);
Whereas:
const countRef = useRef(0);
and:
countRef.current = 10;
does not itself cause a render.
Use useState when the value affects the rendered UI.
Use useRef when you need to preserve a mutable value without triggering rendering, such as DOM references or certain instance-like values.
Lazy State Initialization vs Normal Initialization
Normal:
const [data, setData] = useState(createInitialData());
Initializer function:
const [data, setData] = useState(() => createInitialData());
The second form is useful when creating the initial state is expensive.
For simple values:
useState(0);
useState("");
useState(false);
there is normally no reason to complicate the code with a function.
Resetting State
Sometimes you want to reset state to its original value.
const [name, setName] = useState("Syed");
Reset:
setName("Syed");
For multiple values:
const initialForm = {
name: "",
email: "",
phone: ""
};
const [form, setForm] = useState(initialForm);
Reset:
setForm(initialForm);
A useful pattern is defining the initial state separately when it is reused.
Resetting State to null
For optional data:
const [selectedUser, setSelectedUser] =
useState<User | null>(null);
Select:
setSelectedUser(user);
Clear:
setSelectedUser(null);
This is commonly used for:
Selected item
↓
Open modal
↓
Display item
↓
Close modal
↓
selectedItem = null
State Replacement vs State Mutation
This is one of the most important concepts.
Suppose:
const [user, setUser] = useState({
name: "Syed",
age: 48
});
Avoid:
user.name = "John";
because you are mutating the existing object.
Prefer:
setUser(prev => ({
...prev,
name: "John"
}));
Similarly for arrays:
Avoid:
users.push(newUser);
Prefer:
setUsers(prev => [
...prev,
newUser
]);
The general React pattern is:
Do not mutate existing state
↓
Create a new value
↓
Pass new value to setter
↓
React can process the state update
State Updates Are Compared by Identity
For objects and arrays, JavaScript uses reference identity.
For example:
const user = {
name: "Syed"
};
Creating:
const newUser = {
name: "Syed"
};
creates a different object reference even though the contents look the same.
Therefore:
user === newUser
is:
false
This is why immutable state updates are important.
For example:
setUser(prev => ({
...prev,
name: "John"
}));
creates a new object.
A Complete useState Example
Here is a small example combining several important concepts:
import { useState } from "react";
type User = {
id: number;
name: string;
active: boolean;
};
export function UserManager() {
const [users, setUsers] = useState<User[]>([
{
id: 1,
name: "Syed",
active: true
},
{
id: 2,
name: "John",
active: false
}
]);
const [name, setName] = useState("");
const addUser = () => {
if (!name.trim()) {
return;
}
setUsers(previousUsers => [
...previousUsers,
{
id: Date.now(),
name,
active: true
}
]);
setName("");
};
const toggleUser = (id: number) => {
setUsers(previousUsers =>
previousUsers.map(user =>
user.id === id
? {
...user,
active: !user.active
}
: user
)
);
};
const removeUser = (id: number) => {
setUsers(previousUsers =>
previousUsers.filter(user => user.id !== id)
);
};
return (
<div>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter user name"
/>
<button onClick={addUser}>
Add User
</button>
{users.map(user => (
<div key={user.id}>
<span>
{user.name} -{" "}
{user.active ? "Active" : "Inactive"}
</span>
<button onClick={() => toggleUser(user.id)}>
Toggle
</button>
<button onClick={() => removeUser(user.id)}>
Delete
</button>
</div>
))}
</div>
);
}
This single component demonstrates several useState concepts:
useState
│
├── String state
│ └── name
│
├── Array state
│ └── users
│
├── Functional state updates
│ └── previousUsers => ...
│
├── Immutable array updates
│ ├── map()
│ ├── filter()
│ └── spread operator
│
├── Controlled input
│ └── value + onChange
│
└── Conditional rendering
└── active / inactive
The Most Important useState Patterns
1. Primitive state
const [name, setName] = useState("Syed");
2. Boolean state
const [isOpen, setIsOpen] = useState(false);
3. Counter
const [count, setCount] = useState(0);
setCount(prev => prev + 1);
4. Object state
const [user, setUser] = useState({
name: "Syed",
age: 48
});
5. Update object
setUser(prev => ({
...prev,
name: "John"
}));
6. Array state
const [users, setUsers] = useState<User[]>([]);
7. Add item
setUsers(prev => [
...prev,
newUser
]);
8. Remove item
setUsers(prev =>
prev.filter(user => user.id !== id)
);
9. Update item
setUsers(prev =>
prev.map(user =>
user.id === id
? { ...user, active: true }
: user
)
);
10. Toggle boolean
setIsOpen(prev => !prev);
11. Nullable state
const [selectedUser, setSelectedUser] =
useState<User | null>(null);
12. Lazy initialization
const [data, setData] =
useState(() => createInitialData());
Rules of Hooks and useState
useState is a Hook, so it follows React's Rules of Hooks.
Call it at the top level of the component:
function Component() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
Do not put it inside an if:
if (condition) {
const [count, setCount] = useState(0); // Incorrect
}
Do not put it inside a loop:
for (...) {
const [count, setCount] = useState(0); // Incorrect
}
Do not call it inside an event handler:
function handleClick() {
const [count, setCount] = useState(0); // Incorrect
}
The correct pattern is:
function Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
};
return (
<button onClick={handleClick}>
{count}
</button>
);
}
Why Can't Hooks Be Called Conditionally?
React relies on the consistent ordering of Hooks across renders.
This is valid:
function Component() {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [active, setActive] = useState(false);
// ...
}
React can conceptually associate:
Hook #1 → name
Hook #2 → age
Hook #3 → active
If you conditionally call Hooks, the order could change between renders, making state association unreliable.
Therefore:
Hooks must be called in the same order on every render.
useState Is Local to a Component
State belongs to the component instance that declares it.
For example:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
{count}
</button>
);
}
If you render:
<Counter />
<Counter />
each instance has its own state:
Counter #1
count = 5
Counter #2
count = 2
Changing one does not automatically change the other.
This is an important distinction between component-local state and shared application state.
useState and Child Components
A parent can maintain state and pass it to a child through props.
function Parent() {
const [name, setName] = useState("Syed");
return (
<Child name={name} />
);
}
The child receives:
function Child({ name }: { name: string }) {
return <h3>Hello {name}</h3>;
}
The state belongs to the parent:
Parent
│
│ useState
│
└── name
│
↓
Props
│
↓
Child
This is the foundation of React's one-way data flow.
When Should You Use useState?
Use useState when a value:
- changes over time
- affects the UI
- should persist between renders
- needs to trigger a re-render when changed
Typical examples include:
Form values
Modal visibility
Selected item
Counter
Tabs
Dropdown state
Loading state
Error messages
Search text
Pagination
Filters
User input
Expanded/collapsed sections
Shopping cart items
Authentication-related UI state
When Should You NOT Use useState?
Don't automatically put every variable into state.
For example:
const firstName = "Syed";
const lastName = "Ahmed";
const fullName = `${firstName} ${lastName}`;
fullName does not need state because it can be calculated.
Similarly:
const total = price * quantity;
doesn't necessarily need:
const [total, setTotal] = useState(0);
If total can be derived from existing state, calculate it.
A Simple Mental Model
Think of useState as a connection between your component and React's rendering system:
useState
│
┌──────────┴──────────┐
│ │
Current value Setter function
│ │
state │
│ │
│ setState()
│ │
└──────────┬──────────┘
↓
React update
↓
Re-render
↓
Updated UI
For example:
const [count, setCount] = useState(0);
means:
count
↓
Current state
setCount
↓
Request state update
useState
↓
Connects the state to React rendering
useState Interview Questions to Remember
What is useState?
useState is a React Hook that allows functional components to maintain state and trigger re-rendering when that state is updated.
Does useState persist data between renders?
Yes. React preserves state between renders of the component.
Does changing a normal variable trigger a re-render?
No.
What does the setter function do?
It requests a state update and causes React to schedule a re-render when appropriate.
When should you use functional state updates?
When the new state depends on the previous state:
setCount(prev => prev + 1);
Does useState merge objects automatically?
No. Object state is replaced. Use the spread operator when you need to preserve existing properties.
setUser(prev => ({
...prev,
name: "John"
}));
Can useState store arrays?
Yes.
const [users, setUsers] = useState<User[]>([]);
Can useState store objects?
Yes.
const [user, setUser] = useState<User | null>(null);
Can useState store null?
Yes, and TypeScript can explicitly represent that with a union type.
useState<User | null>(null);
Can you call useState inside an if?
No. Hooks must be called at the top level of the component.
Does useState cause the component to render immediately while the current function is executing?
No. A state setter schedules an update; the current render/event handler continues using its existing state snapshot.
Final Summary
useState is the fundamental React Hook for component-local, UI-affecting state.
The core pattern is:
const [state, setState] = useState(initialValue);
The most important patterns are:
// String
const [name, setName] = useState("Syed");
// Number
const [count, setCount] = useState(0);
// Boolean
const [isOpen, setIsOpen] = useState(false);
// Array
const [users, setUsers] = useState<User[]>([]);
// Object
const [user, setUser] = useState<User | null>(null);
// Nullable value
const [message, setMessage] = useState<string | null>(null);
// Functional update
setCount(prev => prev + 1);
// Toggle
setIsOpen(prev => !prev);
// Add array item
setUsers(prev => [...prev, newUser]);
// Remove array item
setUsers(prev => prev.filter(x => x.id !== id));
// Update array item
setUsers(prev =>
prev.map(x =>
x.id === id ? { ...x, active: true } : x
)
);
// Update object
setUser(prev => ({
...prev!,
name: "John"
}));
The three rules worth remembering most are:
1. State changes should use the setter.
setCount(10)
2. If next state depends on previous state, use the functional updater.
setCount(prev => prev + 1)
3. Don't mutate objects/arrays directly.
Create a new object/array and give it to the setter.
Once you understand useState, the next important React concepts naturally build on it: state batching, state snapshots, lifting state up, sharing state between components, useEffect, useReducer, and Context API.