React Beginner

State Management in React - Managing Component Data

CodingerWeb
CodingerWeb
18 views 60 min read

What is State in React?

State is a built-in React object that allows components to create and manage their own data. Unlike props, which are passed down from parent components, state is managed locally within a component and can change over time, triggering re-renders when updated.

Understanding State vs Props

State Props
Managed within the component Passed from parent component
Can be changed (mutable) Read-only (immutable)
Triggers re-render when changed Received from parent
Private to the component Can be shared between components

Using useState Hook

The useState hook is the primary way to add state to functional components:

import React, { useState } from 'react';

function Counter() {
  // Declare state variable with initial value
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };
  
  const decrement = () => {
    setCount(count - 1);
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div className="counter">
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

State with Different Data Types

State can hold various types of data:

function UserProfile() {
  // String state
  const [name, setName] = useState("John Doe");
  
  // Number state
  const [age, setAge] = useState(25);
  
  // Boolean state
  const [isVisible, setIsVisible] = useState(true);
  
  // Array state
  const [hobbies, setHobbies] = useState(["Reading", "Gaming"]);
  
  // Object state
  const [user, setUser] = useState({
    email: "john@example.com",
    location: "New York",
    preferences: {
      theme: "dark",
      notifications: true
    }
  });
  
  const addHobby = () => {
    const newHobby = prompt("Enter a new hobby:");
    if (newHobby) {
      setHobbies([...hobbies, newHobby]);
    }
  };
  
  const updateEmail = () => {
    const newEmail = prompt("Enter new email:");
    if (newEmail) {
      setUser({
        ...user,
        email: newEmail
      });
    }
  };
  
  const toggleTheme = () => {
    setUser({
      ...user,
      preferences: {
        ...user.preferences,
        theme: user.preferences.theme === "dark" ? "light" : "dark"
      }
    });
  };
  
  return (
    <div className="user-profile">
      {isVisible && (
        <div>
          <h2>{name}, {age} years old</h2>
          <p>Email: {user.email}</p>
          <p>Location: {user.location}</p>
          <p>Theme: {user.preferences.theme}</p>
          
          <div>
            <h3>Hobbies:</h3>
            <ul>
              {hobbies.map((hobby, index) => (
                <li key={index}>{hobby}</li>
              ))}
            </ul>
            <button onClick={addHobby}>Add Hobby</button>
          </div>
          
          <div>
            <button onClick={updateEmail}>Update Email</button>
            <button onClick={toggleTheme}>Toggle Theme</button>
            <button onClick={() => setIsVisible(false)}>
              Hide Profile
            </button>
          </div>
        </div>
      )}
      
      {!isVisible && (
        <button onClick={() => setIsVisible(true)}>
          Show Profile
        </button>
      )}
    </div>
  );
}

Functional State Updates

When the new state depends on the previous state, use functional updates:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState("");
  
  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        {
          id: Date.now(),
          text: inputValue,
          completed: false
        }
      ]);
      setInputValue("");
    }
  };
  
  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };
  
  const deleteTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.filter(todo => todo.id !== id)
    );
  };
  
  return (
    <div className="todo-list">
      <h2>Todo List</h2>
      
      <div className="add-todo">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Enter a new todo"
          onKeyPress={(e) => e.key === "Enter" && addTodo()}
        />
        <button onClick={addTodo}>Add</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id} className={todo.completed ? "completed" : ""}>
            <span onClick={() => toggleTodo(todo.id)}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>
              Delete
            </button>
          </li>
        ))}
      </ul>
      
      <p>
        Total: {todos.length} | 
        Completed: {todos.filter(t => t.completed).length} | 
        Remaining: {todos.filter(t => !t.completed).length}
      </p>
    </div>
  );
}

Multiple State Variables vs Single State Object

You can choose between multiple useState calls or a single state object:

// Multiple state variables (recommended for simple, unrelated data)
function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");
  
  // ... component logic
}

// Single state object (good for related data)
function LoginForm() {
  const [formState, setFormState] = useState({
    email: "",
    password: "",
    isLoading: false,
    error: ""
  });
  
  const updateField = (field, value) => {
    setFormState(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    setFormState(prev => ({
      ...prev,
      isLoading: true,
      error: ""
    }));
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log("Login successful!");
    } catch (err) {
      setFormState(prev => ({
        ...prev,
        error: "Login failed. Please try again."
      }));
    } finally {
      setFormState(prev => ({
        ...prev,
        isLoading: false
      }));
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={formState.email}
          onChange={(e) => updateField("email", e.target.value)}
          placeholder="Email"
          required
        />
      </div>
      
      <div>
        <input
          type="password"
          value={formState.password}
          onChange={(e) => updateField("password", e.target.value)}
          placeholder="Password"
          required
        />
      </div>
      
      {formState.error && (
        <div className="error">{formState.error}</div>
      )}
      
      <button type="submit" disabled={formState.isLoading}>
        {formState.isLoading ? "Logging in..." : "Login"}
      </button>
    </form>
  );
}

State and Event Handling

State is commonly updated in response to user events:

function InteractiveCard() {
  const [isExpanded, setIsExpanded] = useState(false);
  const [likes, setLikes] = useState(0);
  const [isLiked, setIsLiked] = useState(false);
  
  const toggleExpanded = () => {
    setIsExpanded(!isExpanded);
  };
  
  const handleLike = () => {
    if (isLiked) {
      setLikes(likes - 1);
      setIsLiked(false);
    } else {
      setLikes(likes + 1);
      setIsLiked(true);
    }
  };
  
  return (
    <div className="interactive-card">
      <div className="card-header">
        <h3>Interactive Card</h3>
        <button onClick={toggleExpanded}>
          {isExpanded ? "Collapse" : "Expand"}
        </button>
      </div>
      
      <div className="card-content">
        <p>This is always visible content.</p>
        
        {isExpanded && (
          <div className="expanded-content">
            <p>This content is only visible when expanded!</p>
            <p>You can put any additional information here.</p>
          </div>
        )}
      </div>
      
      <div className="card-footer">
        <button 
          onClick={handleLike}
          className={isLiked ? "liked" : ""}
        >
          ❤️ {likes} {likes === 1 ? "Like" : "Likes"}
        </button>
      </div>
    </div>
  );
}

Practical Exercise: Shopping Cart

function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [newItem, setNewItem] = useState({ name: "", price: "" });
  
  const addItem = () => {
    if (newItem.name && newItem.price) {
      setItems(prevItems => [
        ...prevItems,
        {
          id: Date.now(),
          name: newItem.name,
          price: parseFloat(newItem.price),
          quantity: 1
        }
      ]);
      setNewItem({ name: "", price: "" });
    }
  };
  
  const updateQuantity = (id, newQuantity) => {
    if (newQuantity <= 0) {
      removeItem(id);
      return;
    }
    
    setItems(prevItems =>
      prevItems.map(item =>
        item.id === id 
          ? { ...item, quantity: newQuantity }
          : item
      )
    );
  };
  
  const removeItem = (id) => {
    setItems(prevItems =>
      prevItems.filter(item => item.id !== id)
    );
  };
  
  const getTotalPrice = () => {
    return items.reduce((total, item) => 
      total + (item.price * item.quantity), 0
    ).toFixed(2);
  };
  
  const getTotalItems = () => {
    return items.reduce((total, item) => total + item.quantity, 0);
  };
  
  return (
    <div className="shopping-cart">
      <h2>Shopping Cart ({getTotalItems()} items)</h2>
      
      <div className="add-item">
        <input
          type="text"
          placeholder="Item name"
          value={newItem.name}
          onChange={(e) => setNewItem({...newItem, name: e.target.value})}
        />
        <input
          type="number"
          placeholder="Price"
          value={newItem.price}
          onChange={(e) => setNewItem({...newItem, price: e.target.value})}
        />
        <button onClick={addItem}>Add Item</button>
      </div>
      
      <div className="cart-items">
        {items.length === 0 ? (
          <p>Your cart is empty</p>
        ) : (
          items.map(item => (
            <div key={item.id} className="cart-item">
              <span>{item.name}</span>
              <span>${item.price.toFixed(2)}</span>
              <div>
                <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
                  -
                </button>
                <span>{item.quantity}</span>
                <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
                  +
                </button>
              </div>
              <button onClick={() => removeItem(item.id)}>
                Remove
              </button>
            </div>
          ))
        )}
      </div>
      
      {items.length > 0 && (
        <div className="cart-total">
          <h3>Total: ${getTotalPrice()}</h3>
          <button onClick={() => setItems([])}>
            Clear Cart
          </button>
        </div>
      )}
    </div>
  );
}

Best Practices for State Management

  • Keep state minimal: Only store what you need in state
  • Use functional updates: When new state depends on previous state
  • Don't mutate state directly: Always create new objects/arrays
  • Group related state: Use objects for related data
  • Initialize state properly: Provide appropriate initial values
  • Use multiple useState calls: For unrelated pieces of state

Common Mistakes to Avoid

  • Mutating state directly: Always use the setter function
  • Not using functional updates: Can lead to stale state issues
  • Too much state: Not everything needs to be in state
  • Forgetting dependencies: In useEffect (covered in later lessons)

Summary

State is fundamental to creating interactive React applications. You've learned how to use the useState hook, manage different types of state data, handle user interactions, and follow best practices for state management. In the next lesson, we'll explore event handling in more detail and learn how to create more interactive user interfaces.