← Back to Articles         
useState - React Hook

useState - React Hook

Published on 11 Nov 2026 24 min read React
React Fundamentals

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 value
  • setCount → function used to update that state
  • 0 → 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.

← Back to Articles