State Basics

What is State?

State is a JavaScript object that holds data which may change over time and affects what a component renders. Unlike props which are passed from parent components, state is managed internally by a component itself.

Why Do We Need State?

Without state, components would be static and couldn't:

  • Respond to user interactions (clicks, form inputs, etc.)
  • Change what's displayed based on user actions
  • Update after fetching data from an API
  • Remember information across renders

State vs. Props

StateProps
Managed within the componentReceived from parent components
Can change over timeRead-only within the component
Causes re-renders when updatedComponent re-renders if props change
Private to the componentCan be passed down to child components

Using State in Function Components

In function components, we add and use state with the useState hook:

import { useState } from "react";

const Counter = () => {
  // 1. Declare a state variable named "count" with initial value of 0
  // 2. Get a function "setCount" to update this variable
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};

Understanding useState

The useState hook:

const [state, setState] = useState(initialValue);
  • Takes an initial value for your state
  • Returns an array with exactly two items:
    1. The current state value
    2. A function to update that state value
  • We typically use array destructuring to give these items descriptive names

Multiple State Variables

You can use useState multiple times in a single component to track different values:

const UserForm = () => {
  const [username, setUsername] = useState("");
  const [age, setAge] = useState(0);
  const [isSubmitted, setIsSubmitted] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitted(true);
    console.log(`Username: ${username}, Age: ${age}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input id="username" value={username} onChange={(e) => setUsername(e.target.value)} />
      </div>

      <div>
        <label htmlFor="age">Age:</label>
        <input
          id="age"
          type="number"
          value={age}
          onChange={(e) => setAge(parseInt(e.target.value) || 0)}
        />
      </div>

      <button type="submit">Submit</button>

      {isSubmitted && <p>Thank you for submitting!</p>}
    </form>
  );
};

Properly Updating State

Rules for Updating State

  1. Never modify state directly

    // WRONG
    count = count + 1;
    
    // CORRECT
    setCount(count + 1);
    
  2. State updates may be batched or delayed Multiple state updates in the same function may be batched together for performance.

  3. State updates are not immediate The state variable doesn't change right after calling the state update function:

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

Functional Updates

When new state depends on the previous state, use the functional form of the state updater:

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

  // Better approach when new state depends on old state
  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  // This doesn't work correctly with rapidly repeated clicks
  const incrementMultiple = () => {
    setCount(count + 1); // Both use same 'count' value
    setCount(count + 1); // So this just sets it to the same value twice
  };

  // This works correctly
  const incrementMultipleBetter = () => {
    setCount((prevCount) => prevCount + 1); // Uses latest value
    setCount((prevCount) => prevCount + 1); // Also uses latest value
  };

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

State with Different Data Types

Booleans

const ToggleButton = () => {
  const [isOn, setIsOn] = useState(false);

  return <button onClick={() => setIsOn(!isOn)}>{isOn ? "ON" : "OFF"}</button>;
};

Numbers

const StepCounter = () => {
  const [steps, setSteps] = useState(0);

  return (
    <div>
      <p>You've walked {steps} steps today</p>
      <button onClick={() => setSteps(steps + 100)}>Record Walk</button>
      <button onClick={() => setSteps(0)}>Reset Counter</button>
    </div>
  );
};

Strings

const Greeting = () => {
  const [name, setName] = useState("");

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter your name" />
      <p>{name ? `Hello, ${name}!` : "Please enter your name"}</p>
    </div>
  );
};

Objects

When working with objects in state, always create a new object instead of modifying the existing one:

const UserProfile = () => {
  const [user, setUser] = useState({
    firstName: "",
    lastName: "",
    email: "",
  });

  // Updating a single property while preserving others
  const updateEmail = (newEmail) => {
    setUser({
      ...user, // Spread the existing properties
      email: newEmail, // Override just the email
    });
  };

  // Generic handler for multiple input fields
  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser({
      ...user,
      [name]: value, // Using computed property name
    });
  };

  return (
    <form>
      <div>
        <input
          name="firstName"
          value={user.firstName}
          onChange={handleChange}
          placeholder="First Name"
        />
      </div>
      <div>
        <input
          name="lastName"
          value={user.lastName}
          onChange={handleChange}
          placeholder="Last Name"
        />
      </div>
      <div>
        <input name="email" value={user.email} onChange={handleChange} placeholder="Email" />
      </div>
      <div>
        <button type="button" onClick={() => updateEmail("test@example.com")}>
          Set Test Email
        </button>
      </div>
    </form>
  );
};

Arrays

Working with arrays in state also requires creating new arrays instead of modifying existing ones:

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState("");

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

    // Create a new array with the new item
    setTodos([...todos, input]);
    setInput(""); // Clear input after adding
  };

  const removeTodo = (index) => {
    // Create a new array without the item at index
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <div>
      <h2>Todo List</h2>
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add a new task"
        />
        <button onClick={addTodo}>Add</button>
      </div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

Common State Patterns

Toggle State

const Accordion = ({ title, content }) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="accordion">
      <div className="accordion-header" onClick={() => setIsOpen(!isOpen)}>
        <h3>{title}</h3>
        <span>{isOpen ? "▲" : "▼"}</span>
      </div>

      {isOpen && <div className="accordion-content">{content}</div>}
    </div>
  );
};

Form Fields with State

const SimpleForm = () => {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });

  const [isSubmitted, setIsSubmitted] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("Submitting form data:", formData);
    setIsSubmitted(true);
  };

  if (isSubmitted) {
    return <div>Thank you for your submission!</div>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input id="name" name="name" value={formData.name} onChange={handleChange} required />
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>

      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          required
        />
      </div>

      <button type="submit">Submit</button>
    </form>
  );
};

Loading State

const DataDisplay = () => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = () => {
    setIsLoading(true);
    setError(null);

    // Simulate an API call
    setTimeout(() => {
      try {
        // Mock success
        setData({ name: "Sample Data", value: 42 });
        setIsLoading(false);
      } catch (err) {
        setError("Failed to fetch data");
        setIsLoading(false);
      }
    }, 1500);
  };

  return (
    <div>
      <button onClick={fetchData} disabled={isLoading}>
        {isLoading ? "Loading..." : "Fetch Data"}
      </button>

      {error && <div className="error">{error}</div>}

      {data && !isLoading && (
        <div>
          <h3>Data Loaded:</h3>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  );
};

State Between Parent and Child Components

One of the most important patterns in React is passing state between parent and child components:

// The parent manages state and passes it down to children
const ParentComponent = () => {
  const [selectedItem, setSelectedItem] = useState(null);

  const items = [
    { id: 1, name: "Item 1" },
    { id: 2, name: "Item 2" },
    { id: 3, name: "Item 3" },
  ];

  return (
    <div>
      <h2>Select an Item</h2>

      <ItemList items={items} onItemSelect={setSelectedItem} selectedItem={selectedItem} />

      {selectedItem && <ItemDetail item={selectedItem} />}
    </div>
  );
};

// Child component gets props from parent and uses them
const ItemList = ({ items, onItemSelect, selectedItem }) => {
  return (
    <ul className="item-list">
      {items.map((item) => (
        <li
          key={item.id}
          className={selectedItem?.id === item.id ? "selected" : ""}
          onClick={() => onItemSelect(item)}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
};

// Another child component uses props from parent
const ItemDetail = ({ item }) => {
  return (
    <div className="item-detail">
      <h3>{item.name} Details</h3>
      <p>You selected item #{item.id}</p>
    </div>
  );
};

This pattern demonstrates:

  1. The parent manages the state (selectedItem)
  2. The state is passed down to children as props
  3. Event handlers are passed down to allow children to update parent state
  4. When state changes, all affected components re-render

Lifting State Up

When multiple components need the same state, move it up to their closest common ancestor:

const TemperatureCalculator = () => {
  // State is "lifted" to the parent component
  const [temperature, setTemperature] = useState(0);
  const [scale, setScale] = useState("c"); // 'c' for Celsius, 'f' for Fahrenheit

  // Conversion functions
  const toCelsius = (fahrenheit) => {
    return ((fahrenheit - 32) * 5) / 9;
  };

  const toFahrenheit = (celsius) => {
    return (celsius * 9) / 5 + 32;
  };

  // Handle input from either control
  const handleCelsiusChange = (value) => {
    setTemperature(value);
    setScale("c");
  };

  const handleFahrenheitChange = (value) => {
    setTemperature(value);
    setScale("f");
  };

  // Calculate both temperatures based on current scale and value
  const celsius = scale === "f" ? toCelsius(temperature) : temperature;
  const fahrenheit = scale === "c" ? toFahrenheit(temperature) : temperature;

  return (
    <div>
      <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={handleCelsiusChange} />

      <TemperatureInput
        scale="f"
        temperature={fahrenheit}
        onTemperatureChange={handleFahrenheitChange}
      />

      <BoilingVerdict celsius={celsius} />
    </div>
  );
};

const TemperatureInput = ({ scale, temperature, onTemperatureChange }) => {
  const scaleNames = {
    c: "Celsius",
    f: "Fahrenheit",
  };

  const handleChange = (e) => {
    // Convert string to number or use empty string for invalid input
    const value = e.target.value === "" ? "" : parseFloat(e.target.value);
    onTemperatureChange(value);
  };

  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input value={temperature} onChange={handleChange} />
    </fieldset>
  );
};

const BoilingVerdict = ({ celsius }) => {
  if (celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
};

Best Practices for State Management

1. Keep state as local as possible

Only move state up the tree when it truly needs to be shared.

2. Use derived values instead of additional state

// Bad: Using extra state that could be derived
const Counter = () => {
  const [count, setCount] = useState(0);
  const [isPositive, setIsPositive] = useState(false);

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    setIsPositive(newCount > 0); // Unnecessary extra state update
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>{isPositive ? "Positive" : "Zero or negative"}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

// Good: Calculate derived values during render
const Counter = () => {
  const [count, setCount] = useState(0);

  // This is calculated during render, not stored in state
  const isPositive = count > 0;

  return (
    <div>
      <p>Count: {count}</p>
      <p>{isPositive ? "Positive" : "Zero or negative"}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

3. Group related state

If multiple state variables are always updated together, consider combining them:

// Before: Separate state variables
const LoginForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // Login logic...
};

// After: Grouped related state
const LoginForm = () => {
  const [form, setForm] = useState({
    username: "",
    password: "",
  });

  const [status, setStatus] = useState({
    isLoading: false,
    error: null,
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({
      ...form,
      [name]: value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus({ isLoading: true, error: null });

    try {
      // Login logic...
      // On success:
      setStatus({ isLoading: false, error: null });
    } catch (err) {
      setStatus({ isLoading: false, error: err.message });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {status.error && <div className="error">{status.error}</div>}

      <div>
        <input name="username" value={form.username} onChange={handleChange} />
      </div>

      <div>
        <input type="password" name="password" value={form.password} onChange={handleChange} />
      </div>

      <button type="submit" disabled={status.isLoading}>
        {status.isLoading ? "Logging in..." : "Login"}
      </button>
    </form>
  );
};

4. Avoid redundant state

Don't store information in state that can be computed from props or other state:

// Bad: Redundant state
const UserProfile = ({ user }) => {
  // Don't do this! It creates a copy that won't update if user prop changes
  const [userData, setUserData] = useState(user);

  return <div>{userData.name}</div>;
};

// Good: Use props directly
const UserProfile = ({ user }) => {
  return <div>{user.name}</div>;
};

Exercises

Exercise 1: Basic Counter

Objective: Create a counter component with buttons to increase, decrease, and reset the count.

Detailed Instructions:

  1. Create a new file called counter.tsx in your components folder.
  2. Import React and the useState hook from the 'react' package.
  3. Create and export a functional component named Counter.
  4. Inside your component:
    • Create a state variable called count with an initial value of 0 using the useState hook.
    • Create a return statement with a div that contains:
      • An h2 element to display the current count value
      • A button labeled "Decrement" that decreases the count by 1 when clicked
      • A button labeled "Increment" that increases the count by 1 when clicked
      • A button labeled "Reset" that sets the count back to 0 when clicked
  5. For each button, create an onClick handler that uses the setCount function to update the state.
  6. Remember to export your component so it can be used in other files.

Hints:

  • Use the setCount function provided by useState to update the count.
  • For the "Decrement" button, use setCount(count - 1).
  • For the "Increment" button, use setCount(count + 1).
  • For the "Reset" button, use setCount(0).

Expected Result: When rendered, your component should display the current count and three buttons. Clicking "Increment" should increase the count by 1, clicking "Decrement" should decrease it by 1, and clicking "Reset" should set it back to 0.

Exercise 2: Toggle Visibility

Objective: Create a component with a button that shows and hides content when clicked.

Detailed Instructions:

  1. Create a new file called toggle-content.tsx in your components folder.
  2. Import React and the useState hook from the 'react' package.
  3. Create and export a functional component named ToggleContent.
  4. Inside your component:
    • Create a state variable called isVisible with an initial value of false using the useState hook.
    • Create a return statement with a div that contains:
      • A button that toggles the isVisible state when clicked
      • Some content (multiple paragraphs or other elements) that should only be visible when isVisible is true
  5. Use conditional rendering to show or hide the content based on the isVisible state.
  6. Make the button text dynamic - it should say "Show Content" when content is hidden and "Hide Content" when content is visible.

Hints:

  • Use the logical AND operator (&&) for conditional rendering: {isVisible && <div>Content</div>}
  • To toggle a boolean state, use the NOT operator (!): setIsVisible(!isVisible)
  • Use a ternary operator for the button text: {isVisible ? 'Hide Content' : 'Show Content'}

Expected Result: When rendered, your component should display only a button labeled "Show Content". When clicked, additional content should appear, and the button text should change to "Hide Content". Clicking again should hide the content and change the button text back to "Show Content".

Exercise 3: Form with Multiple Fields

Objective: Create a contact form that uses state to manage multiple input fields and form submission.

Detailed Instructions:

  1. Create a new file called contact-form.tsx in your components folder.
  2. Import React and the useState hook from the 'react' package.
  3. Create and export a functional component named ContactForm.
  4. Inside your component:
    • Create a state object called formData with properties for name, email, and message, all initialized as empty strings.
    • Create another state variable called submitted with an initial value of false.
    • Create a function called handleChange that updates the appropriate property in the formData state when any input changes.
    • Create a function called handleSubmit that prevents the default form submission, logs the form data, and sets submitted to true.
  5. Create conditional rendering to show either:
    • The form with input fields for name, email, and message when submitted is false.
    • A success message and a button to submit another message when submitted is true.
  6. Remember to:
    • Use the value attribute on each input to make it a controlled component.
    • Connect each input to the handleChange function.
    • Add proper labels for accessibility.
    • Include a submit button in the form.

Hints:

  • For object state updates, remember to spread the existing state: {...formData, [name]: value}
  • Use name attributes on inputs to identify which field to update in the handleChange function.
  • Use the required attribute on inputs for basic validation.
  • For the form submission handler, don't forget e.preventDefault() to stop the page from reloading.

Expected Result: A form with three input fields (name, email, message) and a submit button. When the form is submitted, it should be replaced with a success message and a button to submit another message. Clicking that button should reset the form.

Exercise 4: Shopping List

Objective: Create a shopping list component that allows users to add, check off, and remove items.

Detailed Instructions:

  1. Create a new file called shopping-list.tsx in your components folder.
  2. Import React and the useState hook from the 'react' package.
  3. Create and export a functional component named ShoppingList.
  4. Inside your component:
    • Create a state array called items initialized as an empty array. Each item should eventually be an object with id, name, and completed properties.
    • Create a state variable called inputValue initialized as an empty string to track what the user types in the input field.
    • Create three functions:
      • handleAddItem: Adds a new item to the list when the user clicks the Add button.
      • handleToggleItem: Marks an item as completed or not completed when the user clicks the checkbox.
      • handleRemoveItem: Removes an item from the list when the user clicks the Remove button.
  5. Create a user interface with:
    • An input field for typing new items
    • An "Add" button to add items to the list
    • A list of existing items, where each item has:
      • A checkbox to mark it as completed
      • The item name (crossed out when completed)
      • A "Remove" button to delete the item
  6. If the list is empty, display a message saying "Your shopping list is empty."

Hints:

  • Generate unique IDs for new items with Date.now() or a similar method.
  • For the handleAddItem function, remember to:
    • Check if the input is empty (don't add empty items)
    • Create a new item object with id, name, and completed properties
    • Add the new item to the array using the spread operator
    • Clear the input field after adding
  • For the handleToggleItem function, use map to create a new array where the target item has its completed property toggled.
  • For the handleRemoveItem function, use filter to create a new array without the item to remove.
  • Use conditional styling with textDecoration: item.completed ? 'line-through' : 'none' to show completed items as crossed out.

Expected Result: A fully functional shopping list where users can add new items, mark items as completed (which shows them crossed out), and remove items from the list. The input field should clear after adding an item.

Conclusion

State management is a fundamental concept in React that allows you to build dynamic, interactive user interfaces. By understanding how to properly use useState, you can:

  • Create components that respond to user input
  • Track values that change over time
  • Update the UI automatically when data changes
  • Share data between components through props

Remember these key principles:

  1. Always use the setter function to update state
  2. Treat state as immutable - create new objects/arrays instead of modifying existing ones
  3. Keep state as simple as possible and derive values where you can
  4. Lift state up when multiple components need access to the same data

In the next section, we'll explore more advanced patterns and hooks for managing complex state and side effects in your React applications.