AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

useState Explained with Simple Examples

Last updated:

A complete guide to React's most fundamental hook. Learn how to manage state, handle objects, and use functional updates correctly.

The useState hook is one of the most fundamental features in modern React development. If you're building functional components and need to manage data that changes over time, useState is your go-to tool. In this guide, we'll explore everything you need to know about useState, from basic syntax to real-world patterns that will make you a more confident React developer.

Table of Contents

  1. What is useState?
  2. Why We Need useState
  3. Syntax and Basic Usage
  4. Initializing State
  5. Updating State Values
  6. State Update Functions
  7. Multiple State Variables
  8. Practical Examples
  9. Common Patterns and Mistakes
  10. FAQ

What is useState?

useState is a React Hook that lets you add state to functional components. Before Hooks were introduced in React 16.8, only class components could manage internal state. With useState, functional components gained the same capability, making them the standard approach for building React applications.

State represents data that can change over time—things like user input, toggle switches, counters, form submissions, or fetched data. When state changes, React automatically re-evaluates your component and updates the user interface to reflect the new information.

Think of state as the component's memory. Without it, your component would be "stateless"—it would always render the same output regardless of user interactions.

Why We Need useState

Consider a simple scenario: you have a button that needs to count how many times it's been clicked. Without state, clicking the button wouldn't do anything visible because there's no way to track or remember the click count.

javascript
// ❌ This won't work - no state tracking
function ClickCounter() {
  let count = 0;
  
  const handleClick = () => {
    count++;  // This updates a variable, but React doesn't know to re-render
  };
  
  return (
    <div>
      <p>Clicks: {count}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

When you click the button, count increases internally, but React has no idea this happened. The component never re-renders, so the UI stays frozen at "Clicks: 0". This is where useState comes in—it tells React to track changes and update the UI accordingly.

Syntax and Basic Usage

The basic syntax for useState is straightforward. You import it from React, call it inside your component, and destructure the result into a state variable and a setter function.

javascript
import { useState } from 'react';

Then, inside your component:

javascript
const [count, setCount] = useState(0);

This line does three things:

  1. Imports the hook from React
  2. Declares a state variable (count) initialized to 0
  3. Creates a setter function (setCount) to update that state variable

When you call setCount with a new value, React schedules a re-render of your component with the updated state.

Understanding the Array Destructuring

useState returns an array with exactly two elements. Using array destructuring (the square brackets syntax) is a convention, but you could technically do this:

javascript
const stateArray = useState(0);
const count = stateArray[0];        // Current state value
const setCount = stateArray[1];     // Setter function

However, destructuring is cleaner and more readable:

javascript
const [count, setCount] = useState(0);

Naming Convention

By convention, you name the state variable whatever makes sense for your data, and the setter function is the state variable name with set prefix in camelCase:

  • const [count, setCount] = useState(0)
  • const [name, setName] = useState('')
  • const [isOpen, setIsOpen] = useState(false)
  • const [user, setUser] = useState(null)

This makes code self-documenting and easy to understand at a glance.

Initializing State

The argument you pass to useState() is the initial state value. This value is used only on the component's first render; subsequent renders use the current state value instead.

Initializing with Different Types

You can initialize state with any JavaScript value type:

TypeScript Version

typescript
import { useState } from 'react';

type MyComponentProps = {};

export default function MyComponent() {
  // Number
  const [count, setCount] = useState<number>(0);

  // String
  const [name, setName] = useState<string>('');

  // Boolean
  const [isActive, setIsActive] = useState<boolean>(false);

  // Array
  const [items, setItems] = useState<string[]>([]);

  // Object
  const [user, setUser] = useState<{ name: string; age: number }>({
    name: '',
    age: 0,
  });

  // Null (common for optional data)
  const [data, setData] = useState<null | { id: number }>(null);

  return <div>Component with various state types</div>;
}

JavaScript Version

javascript
import { useState } from 'react';

export default function MyComponent() {
  // Number
  const [count, setCount] = useState(0);

  // String
  const [name, setName] = useState('');

  // Boolean
  const [isActive, setIsActive] = useState(false);

  // Array
  const [items, setItems] = useState([]);

  // Object
  const [user, setUser] = useState({ name: '', age: 0 });

  // Null (common for optional data)
  const [data, setData] = useState(null);

  return <div>Component with various state types</div>;
}

Lazy Initialization

If calculating the initial state is expensive (like parsing JSON, running complex logic, or making an API call), you can pass a function instead of a value. React will only call this function on the initial render:

TypeScript Version

typescript
import { useState } from 'react';

function performComplexCalculation(): number {
  // Simulate expensive operation
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return result;
}

export default function MyComponent() {
  // Expensive calculation happens only once
  const [state, setState] = useState<number>(() => {
    // This function is called only on the first render
    const expensiveValue = performComplexCalculation();
    return expensiveValue;
  });

  return <div>{state}</div>;
}

JavaScript Version

javascript
import { useState } from 'react';

function performComplexCalculation() {
  // Simulate expensive operation
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return result;
}

export default function MyComponent() {
  // Expensive calculation happens only once
  const [state, setState] = useState(() => {
    // This function is called only on the first render
    const expensiveValue = performComplexCalculation();
    return expensiveValue;
  });

  return <div>{state}</div>;
}

This pattern is rarely needed for simple components, but it's valuable for performance optimization in certain scenarios.

Updating State Values

There are two ways to update state: passing a new value directly, or using an updater function. Understanding the difference is crucial for writing reliable React code.

Method 1: Direct Value Update

The simplest approach is to pass the new value directly to the setter function:

javascript
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(5);  // Directly set to 5
};

This works fine when your new state doesn't depend on the previous state. However, when you're working with state that builds on itself (like incrementing a counter), it can cause bugs.

Method 2: Updater Function (Recommended)

When your new state depends on the previous state, always use an updater function:

javascript
const [count, setCount] = useState(0);

const handleIncrement = () => {
  // Use updater function for state that depends on previous state
  setCount(prevCount => prevCount + 1);
};

Pro Tip: Use the updater function approach whenever your new state is derived from the previous state. This ensures React uses the most recent state value, especially in scenarios where multiple state updates might be batched together.

Why This Matters: The Batching Issue

React batches state updates for performance. If you rely on the current state value in a direct update, you might get stale data:

TypeScript Version

typescript
import { useState } from 'react';

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

  const handleClick = () => {
    // ❌ Potentially problematic
    setCount(count + 1);  // Uses current count value
    setCount(count + 1);  // Uses same count value (batched)
    setCount(count + 1);  // Uses same count value (batched)
    
    // Result: count becomes 1, not 3
    // React batches these updates and uses the same "count" value for all three
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click 3 times at once</button>
    </div>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

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

  const handleClick = () => {
    // ❌ Potentially problematic
    setCount(count + 1);  // Uses current count value
    setCount(count + 1);  // Uses same count value (batched)
    setCount(count + 1);  // Uses same count value (batched)
    
    // Result: count becomes 1, not 3
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click 3 times at once</button>
    </div>
  );
}

Now with the updater function:

TypeScript Version

typescript
import { useState } from 'react';

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

  const handleClick = () => {
    // ✅ Correct approach
    setCount(prevCount => prevCount + 1);  // First update: 0 → 1
    setCount(prevCount => prevCount + 1);  // Second update: 1 → 2
    setCount(prevCount => prevCount + 1);  // Third update: 2 → 3
    
    // Result: count becomes 3 ✓
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click 3 times at once</button>
    </div>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

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

  const handleClick = () => {
    // ✅ Correct approach
    setCount(prevCount => prevCount + 1);  // First update: 0 → 1
    setCount(prevCount => prevCount + 1);  // Second update: 1 → 2
    setCount(prevCount => prevCount + 1);  // Third update: 2 → 3
    
    // Result: count becomes 3 ✓
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click 3 times at once</button>
    </div>
  );
}

State Update Functions

The updater function receives the current state as its argument and must return the new state value. This is where the real power of useState becomes apparent.

Simple Updater Functions

For numbers and simple values:

javascript
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount * 2);
setCount(prevCount => prevCount - 5);

Updater Functions with Objects and Arrays

When dealing with objects or arrays, remember that you need to create a new object/array reference (React uses reference equality to detect changes):

TypeScript Version

typescript
import { useState } from 'react';

interface User {
  name: string;
  age: number;
  email: string;
}

export default function UserProfile() {
  const [user, setUser] = useState<User>({ 
    name: 'John', 
    age: 30, 
    email: 'john@example.com' 
  });

  // ❌ Don't mutate directly
  const handleBadUpdate = () => {
    setUser(prevUser => {
      prevUser.name = 'Jane';  // Direct mutation won't trigger re-render
      return prevUser;
    });
  };

  // ✅ Create new object with spread operator
  const handleGoodUpdate = () => {
    setUser(prevUser => ({
      ...prevUser,
      name: 'Jane',
    }));
  };

  return <div>User: {user.name}</div>;
}

JavaScript Version

javascript
import { useState } from 'react';

export default function UserProfile() {
  const [user, setUser] = useState({ 
    name: 'John', 
    age: 30, 
    email: 'john@example.com' 
  });

  // ❌ Don't mutate directly
  const handleBadUpdate = () => {
    setUser(prevUser => {
      prevUser.name = 'Jane';  // Direct mutation won't trigger re-render
      return prevUser;
    });
  };

  // ✅ Create new object with spread operator
  const handleGoodUpdate = () => {
    setUser(prevUser => ({
      ...prevUser,
      name: 'Jane',
    }));
  };

  return <div>User: {user.name}</div>;
}

This is a critical concept: React detects state changes by comparing the new state reference with the previous reference. If you mutate the existing object, React doesn't see a change because the reference is the same.

Multiple State Variables

In real components, you'll often need to track multiple pieces of state. You can call useState multiple times:

TypeScript Version

typescript
import { useState, FormEvent } from 'react';

export default function RegistrationForm() {
  const [firstName, setFirstName] = useState<string>('');
  const [lastName, setLastName] = useState<string>('');
  const [email, setEmail] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);

    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log(`Registered: ${firstName} ${lastName} (${email})`);
    } finally {
      setIsLoading(false);
      setFirstName('');
      setLastName('');
      setEmail('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
        placeholder="First name"
      />
      <input
        type="text"
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
        placeholder="Last name"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

export default function RegistrationForm() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);

    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log(`Registered: ${firstName} ${lastName} (${email})`);
    } finally {
      setIsLoading(false);
      setFirstName('');
      setLastName('');
      setEmail('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
        placeholder="First name"
      />
      <input
        type="text"
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
        placeholder="Last name"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
}

Note: While you can call useState multiple times, consider consolidating related state into a single object if you have many state variables. However, for simple cases, multiple useState calls is perfectly fine and often more readable.

Practical Examples

Let's look at real-world scenarios where useState shines:

Example 1: Toggle Component

A simple toggle is one of the most common useState use cases:

TypeScript Version

typescript
import { useState } from 'react';

interface ToggleProps {
  label: string;
}

export default function Toggle({ label }: ToggleProps) {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  return (
    <div className="toggle-container">
      <button 
        onClick={() => setIsOpen(!isOpen)}
        aria-expanded={isOpen}
      >
        {label} {isOpen ? '▼' : '▶'}
      </button>
      {isOpen && (
        <div className="toggle-content">
          <p>This content is now visible!</p>
        </div>
      )}
    </div>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

export default function Toggle({ label }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="toggle-container">
      <button 
        onClick={() => setIsOpen(!isOpen)}
        aria-expanded={isOpen}
      >
        {label} {isOpen ? '▼' : '▶'}
      </button>
      {isOpen && (
        <div className="toggle-content">
          <p>This content is now visible!</p>
        </div>
      )}
    </div>
  );
}

Example 2: Form Input Handler

Managing form inputs is where useState really demonstrates its value:

TypeScript Version

typescript
import { useState, FormEvent, ChangeEvent } from 'react';

interface FormData {
  username: string;
  password: string;
  rememberMe: boolean;
}

export default function LoginForm() {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    password: '',
    rememberMe: false,
  });
  const [errors, setErrors] = useState<Partial<FormData>>({});

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value, type, checked } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const validateForm = (): boolean => {
    const newErrors: Partial<FormData> = {};
    if (!formData.username) newErrors.username = true;
    if (!formData.password) newErrors.password = true;
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (validateForm()) {
      console.log('Form submitted:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
          placeholder="Username"
          style={{ borderColor: errors.username ? 'red' : 'gray' }}
        />
      </div>
      <div>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
          style={{ borderColor: errors.password ? 'red' : 'gray' }}
        />
      </div>
      <label>
        <input
          type="checkbox"
          name="rememberMe"
          checked={formData.rememberMe}
          onChange={handleChange}
        />
        Remember me
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

export default function LoginForm() {
  const [formData, setFormData] = useState({
    username: '',
    password: '',
    rememberMe: false,
  });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const validateForm = () => {
    const newErrors = {};
    if (!formData.username) newErrors.username = true;
    if (!formData.password) newErrors.password = true;
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateForm()) {
      console.log('Form submitted:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
          placeholder="Username"
          style={{ borderColor: errors.username ? 'red' : 'gray' }}
        />
      </div>
      <div>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
          style={{ borderColor: errors.password ? 'red' : 'gray' }}
        />
      </div>
      <label>
        <input
          type="checkbox"
          name="rememberMe"
          checked={formData.rememberMe}
          onChange={handleChange}
        />
        Remember me
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

Example 3: List Management

Managing a dynamic list with useState:

TypeScript Version

typescript
import { useState, FormEvent } from 'react';

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

export default function TodoList() {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [input, setInput] = useState<string>('');
  const [nextId, setNextId] = useState<number>(1);

  const handleAddTodo = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (input.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        { id: nextId, text: input, completed: false },
      ]);
      setNextId(prevId => prevId + 1);
      setInput('');
    }
  };

  const handleToggleTodo = (id: number) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleRemoveTodo = (id: number) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add a new todo"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => handleRemoveTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

JavaScript Version

javascript
import { useState } from 'react';

export default function TodoList() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  const [nextId, setNextId] = useState(1);

  const handleAddTodo = (e) => {
    e.preventDefault();
    if (input.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        { id: nextId, text: input, completed: false },
      ]);
      setNextId(prevId => prevId + 1);
      setInput('');
    }
  };

  const handleToggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const handleRemoveTodo = (id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add a new todo"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => handleRemoveTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Common Patterns and Mistakes

✅ Pattern: Functional Updates for Dependent State

Always use the updater function form when your new state depends on the previous state:

javascript
// Good
setCount(prevCount => prevCount + 1);

// Also fine when independent of previous state
setName('Jane');

❌ Mistake: Mutating State Directly

React doesn't detect mutations to the same object reference:

javascript
// ❌ DON'T DO THIS
const [user, setUser] = useState({ name: 'John', age: 30 });

const handleBadMutation = () => {
  user.name = 'Jane';  // Mutation happens
  setUser(user);       // React doesn't see a change (same reference)
};

// ✅ DO THIS INSTEAD
const handleGoodUpdate = () => {
  setUser(prevUser => ({
    ...prevUser,
    name: 'Jane',  // New object created
  }));
};

❌ Mistake: Calling useState Conditionally

Hooks must be called at the top level of your component function, never inside conditions:

javascript
// ❌ DON'T DO THIS
function MyComponent() {
  if (someCondition) {
    const [state, setState] = useState(0);  // ❌ Invalid!
  }
}

// ✅ DO THIS INSTEAD
function MyComponent() {
  const [state, setState] = useState(0);  // Called at top level
  // Then use state conditionally
  if (someCondition) {
    return <div>{state}</div>;
  }
}

✅ Pattern: Combining Related State

If you have several related state values, consider combining them into a single object:

javascript
// Many separate useState calls
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

// Better: combine related state
const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: '',
});

✅ Pattern: Using useReducer for Complex State

When state logic becomes complex with multiple related updates, consider useReducer:

javascript
import { useReducer } from 'react';

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // More suitable for complex state transitions
}

See the related articles for more on useReducer.

FAQ

Q: Can I use useState in class components?

A: No. Hooks, including useState, are designed specifically for functional components. Class components use this.state and this.setState() instead. However, if you're working with modern React code, you should be using functional components with Hooks.

Q: What happens if I pass the same value to setState?

A: React will skip the re-render because it detects that the state hasn't changed (using shallow comparison). However, with objects and arrays, React compares references, not contents. So if you pass a new object with the same properties, React will still re-render.

Q: Is it bad to have many useState calls in one component?

A: Not necessarily. React handles multiple useState calls efficiently. However, if you have 10+ state variables tracking closely related data, consider consolidating into an object or using useReducer for better organization and maintainability.

Q: Can I use useState in a loop?

A: No. useState must be called at the top level of your component function. Calling it in loops, conditions, or nested functions breaks the Rules of Hooks. Each call to useState must happen in the same order on every render.

Q: How do I track previous state value?

A: You'll need to use a useRef or useEffect hook to achieve this. See the related articles for patterns on tracking previous values.

Q: What's the difference between using an updater function and passing a value directly?

A: The updater function receives the current state and is guaranteed to use the most recent value, even if multiple updates are batched. Passing a value directly uses the state from the closure, which can be stale if multiple updates happen synchronously.

Q: Can I set state asynchronously?

A: State updates are batched and processed asynchronously by React. You cannot await setState. If you need to perform actions after state updates, use the useEffect hook, which runs after state changes.

Q: Should I store function or object references in state?

A: Generally, no. If you need to store a callback function, consider using useCallback. If you have an object that changes frequently, keep primitive values in state instead and compute derived values when needed.


Key Takeaways

The useState hook is fundamental to modern React development. Here are the essential points to remember:

  1. useState lets functional components manage state just like class components with this.state
  2. Always use the updater function form when your new state depends on previous state
  3. Create new objects and arrays, don't mutate existing ones
  4. Call useState at the top level of your component—never conditionally
  5. React batches state updates for performance, so use updater functions to ensure correct values
  6. Multiple useState calls are fine, though you can combine related state into objects
  7. Let TypeScript infer return types - you don't need React.FC or explicit return type annotations in most cases

Next Steps

Now that you understand useState, explore these related topics to deepen your React knowledge:


Last Updated: November 23, 2025

Questions or suggestions? Share your thoughts in the comments below. Happy coding!

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT