AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

What Is State in React? Complete Beginner's Guide

Last updated:
React Context API: Eliminate Prop Drilling Effectively

Understand React state: the foundation of dynamic UI. Learn useState hook, state management patterns, and how React tracks data changes with production examples.

# What Is State in React? Complete Beginner's Guide

State is one of the most fundamental concepts in React. Without understanding state, you can't build interactive applications. Yet many beginners struggle with what state actually is, why it matters, and how it differs from other React concepts like props.

This guide will give you a complete understanding of React state, starting from the absolute basics and building to practical patterns you'll use every day.

# Table of Contents

  1. What Is State?
  2. State vs Props
  3. Why We Need State
  4. The useState Hook
  5. How React Tracks State
  6. Single vs Multiple State Values
  7. State Updates and Re-renders
  8. Common Mistakes
  9. Best Practices
  10. Real-World Examples
  11. FAQ

# What Is State? {#what-is-state}

State is data that changes over time and affects what your component displays.

When the user interacts with your app — typing into a form, clicking a button, toggling a switch — that's state changing. React's job is to notice when state changes and update the UI to reflect those changes.

# State vs Regular Variables

Consider the difference:

typescript
// ❌ WRONG: Regular variable doesn't trigger UI updates
function Counter() {
  let count = 0;

  const increment = () => {
    count = count + 1;
    console.log(count); // Logs 1, 2, 3...
    // But UI doesn't update!
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

// ✅ CORRECT: State triggers UI updates
import { useState } from 'react';

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

  const increment = () => {
    setCount(count + 1); // UI updates automatically
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

Key difference: When you change a regular variable, React has no idea it changed. When you change state, React knows and updates the UI.

# State vs Props {#state-vs-props}

Understanding the difference between state and props is crucial.

Aspect State Props
Source Defined inside component Passed from parent
Ownership Component owns its state Parent owns the prop
Can Change? Yes, via setState No, read-only
Purpose Internal data Pass data down
Triggers Re-render Yes Yes

# Practical Example

typescript
// Parent component
function App() {
  const [userName, setUserName] = useState('Alice'); // State in App

  return (
    <div>
      <UserProfile name={userName} /> {/* Pass as prop */}
      <button onClick={() => setUserName('Bob')}>Change Name</button>
    </div>
  );
}

// Child component
interface UserProfileProps {
  name: string; // Received as prop
}

function UserProfile({ name }: UserProfileProps) {
  // This component CANNOT change the name prop directly
  // It can only receive and display it
  return <h1>Welcome, {name}!</h1>;
}

# Mental Model

Think of it this way:

  • State = Data the component manages internally (like your personal diary)
  • Props = Data passed to the component (like a message from a friend)

A component can change its own state, but it cannot change props passed to it. Props are read-only.

# Why We Need State {#why-state}

Without state, React would be completely static. You couldn't build interactive applications.

# Example: Why State Matters

typescript
// BEFORE: No state = no interactivity
function EmailInput() {
  return <input type="email" placeholder="Enter email" />;
}
// User types in the input, but we can't access what they typed!

// AFTER: With state = full interactivity
import { useState } from 'react';

function EmailInput() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);

    // Validate email
    if (value.includes('@')) {
      setError(''); // Valid
    } else {
      setError('Invalid email'); // Invalid
    }
  };

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        placeholder="Enter email"
      />
      {error && <p className="error">{error}</p>}
    </div>
  );
}

Now the component:

  • ✅ Tracks what the user typed
  • ✅ Validates the input
  • ✅ Shows/hides error messages
  • ✅ Provides real-time feedback

# The useState Hook {#usestate-hook}

useState is the hook that lets you add state to functional components. It's the most commonly used React hook.

# Basic Syntax

typescript
import { useState } from 'react';

const [state, setState] = useState(initialValue);

# Breaking It Down

typescript
import { useState } from 'react';

function Counter() {
  // useState returns an array with two elements:
  // [currentValue, functionToUpdateValue]
  const [count, setCount] = useState(0);
  //   │       │           │
  //   │       │           └─ Initial value
  //   │       └─ Function to update state
  //   └─ Current state value

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

# What useState Returns

typescript
// useState returns an array, but we destructure it
const stateArray = useState(0);
const count = stateArray[0];        // Current value (0)
const setCount = stateArray[1];     // Update function

// Same thing, just more concise:
const [count, setCount] = useState(0);

# Different Initial Values

typescript
// Numbers
const [age, setAge] = useState(25);

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

// Booleans
const [isVisible, setIsVisible] = useState(false);

// Objects
const [user, setUser] = useState({ name: 'John', age: 30 });

// Arrays
const [todos, setTodos] = useState<Todo[]>([]);

// Null
const [data, setData] = useState<Data | null>(null);

# TypeScript with useState

typescript
// ✅ GOOD: Explicit type
const [count, setCount] = useState<number>(0);

// ✅ ALSO GOOD: Type inference (React figures it out)
const [count, setCount] = useState(0); // Inferred as number

// ✅ GOOD: For complex types
interface User {
  id: string;
  name: string;
  email: string;
}

const [user, setUser] = useState<User | null>(null);

# How React Tracks State {#how-react-tracks}

Understanding how React tracks state helps you avoid bugs and write better code.

# React's Internal Process

When you call useState:

  1. Registration — React registers the state value with the component
  2. Storage — React stores this value internally, associated with this component instance
  3. Update Detection — When you call the setter, React knows state changed
  4. Re-render — React re-runs the component function with the new state value
  5. UI Update — React updates only the parts of the UI that need updating

# Visual Example

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

  const handleClick = () => {
    setCount(count + 1); // Tell React: "Update state!"
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

// User clicks button:
// 1. handleClick() runs → setCount(1) called
// 2. React detects state changed: 0 → 1
// 3. React re-runs entire Counter component function
// 4. Now count = 1 inside the function
// 5. JSX is re-evaluated with count = 1
// 6. React updates the DOM: "Count: 1"
// 7. User sees the new number on screen

# State is Local

Each component instance has its own state:

typescript
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// In App component
function App() {
  return (
    <>
      <Counter /> {/* Has its own 'count' state */}
      <Counter /> {/* Has its own separate 'count' state */}
      <Counter /> {/* Has its own separate 'count' state */}
    </>
  );
}

// Each Counter has independent state!
// Clicking one button doesn't affect the others

# Single vs Multiple State Values {#single-vs-multiple}

You can have one state value or many. The choice depends on how the data is related.

# Multiple useState Calls

typescript
// ✅ GOOD: Separate states for independent values
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log({ email, password, rememberMe });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <label>
        <input
          type="checkbox"
          checked={rememberMe}
          onChange={(e) => setRememberMe(e.target.checked)}
        />
        Remember me
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

# Single State Object

Sometimes it makes sense to group related state together:

typescript
// ✅ ALSO GOOD: When values are closely related
interface FormState {
  email: string;
  password: string;
  rememberMe: boolean;
}

function LoginForm() {
  const [form, setForm] = useState<FormState>({
    email: '',
    password: '',
    rememberMe: false,
  });

  const handleChange = (field: keyof FormState, value: any) => {
    // Important: Create new object, don't mutate
    setForm((prev) => ({
      ...prev,
      [field]: value,
    }));
  };

  return (
    <form>
      <input
        value={form.email}
        onChange={(e) => handleChange('email', e.target.value)}
      />
      {/* ... rest of form */}
    </form>
  );
}

# Which to Use?

  • Multiple useState — When values are independent (different form fields)
  • Single object — When values are related and updated together (user profile data)

# State Updates and Re-renders {#updates-rerenders}

Understanding how state updates trigger re-renders is crucial.

# Synchronous State Updates

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

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // ❌ Still logs OLD value!
  };

  return (
    <button onClick={handleClick}>Count: {count}</button>
  );
}

// When you click:
// 1. handleClick() runs
// 2. setCount(1) is called
// 3. console.log(count) → logs 0 (still old value!)
// 4. handleClick() finishes
// 5. React updates state
// 6. Component re-runs
// 7. Now count = 1 inside component

Important: State updates don't happen immediately. They're batched by React.

# Update Functions

For dependent updates, use the update function pattern:

typescript
// ❌ WRONG: Unreliable if multiple updates happen
const increment = () => {
  setCount(count + 1);
  setCount(count + 1); // Might not work as expected!
};

// ✅ CORRECT: Use update function for reliable updates
const increment = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1); // Always works correctly
};

# Re-render Behavior

typescript
function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  console.log('Component rendered'); // Logs every time component re-renders

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setName('Bob')}>Change Name</button>
    </div>
  );
}

// Clicking "+" button:
// - Triggers setCount
// - React re-runs entire component
// - "Component rendered" logs

// Clicking "Change Name" button:
// - Triggers setName
// - React re-runs entire component
// - "Component rendered" logs

# Common Mistakes {#mistakes}

# Mistake 1: Mutating State Directly

typescript
// ❌ WRONG: Mutating state directly
const [user, setUser] = useState({ name: 'John', age: 30 });

const updateUser = () => {
  user.name = 'Bob'; // ❌ Mutation!
  setUser(user); // React might not detect change
};

// ✅ CORRECT: Create new object
const updateUser = () => {
  setUser({ ...user, name: 'Bob' }); // New object
};

# Mistake 2: Not Using Update Function

typescript
// ❌ RISKY: State depends on previous value
const increment = () => {
  setCount(count + 1); // count might be stale
};

// ✅ SAFE: Always get latest value
const increment = () => {
  setCount((prev) => prev + 1); // Always fresh
};

# Mistake 3: Setting State in Render

typescript
// ❌ INFINITE LOOP: Never do this!
function Component() {
  const [count, setCount] = useState(0);

  setCount(count + 1); // ❌ Sets state during render

  // This causes:
  // 1. Component renders
  // 2. setCount called
  // 3. Component re-renders
  // 4. setCount called again
  // ... infinite loop!

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

// ✅ CORRECT: Set state in event handlers
function Component() {
  const [count, setCount] = useState(0);

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

# Mistake 4: Forgetting to Import useState

typescript
// ❌ WRONG: Missing import
function Counter() {
  const [count, setCount] = useState(0); // ❌ useState is not defined
}

// ✅ CORRECT: Import first
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // ✅ Works
}

# Best Practices {#best-practices}

# 1. Keep State as Simple as Possible

typescript
// ❌ AVOID: Storing derived data
const [user, setUser] = useState({
  firstName: 'John',
  lastName: 'Doe',
  fullName: 'John Doe', // Derived, can get out of sync!
});

// ✅ GOOD: Compute derived values
const [user, setUser] = useState({
  firstName: 'John',
  lastName: 'Doe',
});

const fullName = `${user.firstName} ${user.lastName}`; // Computed when needed

# 2. Name Your State Meaningfully

typescript
// ❌ UNCLEAR: Poor naming
const [x, setX] = useState(false);
const [data, setData] = useState([]);

// ✅ CLEAR: Meaningful names
const [isLoading, setIsLoading] = useState(false);
const [todos, setTodos] = useState<Todo[]>([]);
typescript
// ❌ SCATTERED: Related values separate
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

// ✅ ORGANIZED: Related values grouped
const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: '',
});

# 4. Initialize With Correct Type

typescript
// ❌ PROBLEMATIC: React infers wrong type
const [data, setData] = useState(null); // Type: null (not what we want)
const stringValue = data.substring(0, 1); // ❌ Type error!

// ✅ CORRECT: Explicit type annotation
const [data, setData] = useState<string | null>(null);
const stringValue = data?.substring(0, 1); // ✅ Type-safe

# Real-World Examples {#examples}

# Example 1: Toggle Component

typescript
function Toggle() {
  const [isOn, setIsOn] = useState(false);

  return (
    <div>
      <p>Light is {isOn ? 'ON' : 'OFF'}</p>
      <button onClick={() => setIsOn(!isOn)}>
        Toggle
      </button>
    </div>
  );
}

# Example 2: Counter

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

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

# Example 3: Form Input

typescript
function NameInput() {
  const [name, setName] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true);
  };

  if (submitted) {
    return (
      <div>
        <p>Hello, {name}!</p>
        <button onClick={() => setSubmitted(false)}>Edit</button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

# Example 4: Todo List

typescript
interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim() === '') return;

    setTodos([
      ...todos,
      {
        id: Date.now().toString(),
        text: input,
        completed: false,
      },
    ]);
    setInput('');
  };

  const toggleTodo = (id: string) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const removeTodo = (id: string) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add a todo"
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        />
        <button onClick={addTodo}>Add</button>
      </div>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

# FAQ {#faq}

Q: Can I use state without useState?

A: No, in functional components you need useState. (Class components had state before hooks, but they're not recommended anymore.)

Q: Should state always start as empty?

A: No, state can have any initial value. Use whatever makes sense: useState(0), useState(''), useState(null), useState({}), etc.

Q: Does changing state immediately update the component?

A: No, React batches state updates. The component re-runs after the current event handler finishes.

Q: Can a component share its state with another component?

A: Not directly. You'd need to "lift state up" to a parent component and pass it down as props.

Q: What's the difference between setCount(5) and setCount(prev => prev + 5)?

A: The first sets count to 5. The second increments by 5. The second is safer when the update depends on the previous value.

Q: Can I update multiple pieces of state in one action?

A: Yes, you can call multiple setState functions in an event handler. React batches them together.

Q: Is there a maximum number of useState calls?

A: No, but it's generally better to split components if they have many independent state values.

Q: Can I delete state?

A: No, state persists as long as the component is mounted. You can set it to null, empty string, empty array, etc.


Master the Foundation: State is the foundation of interactive React applications. Once you fully understand it, every other React concept becomes easier. Practice building small components with state, and you'll develop solid intuition.

Next Steps: Learn about lifting state up to share state between components, explore more hooks like useEffect, and discover state management patterns for complex applications.

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT