javascript-today

State with useState

State - Making Components Interactive

State is data that changes over time. While props are passed from parent to child, state is managed within the component itself. State is what makes React components interactive.

What is State?

State is a component’s memory - data it needs to remember between renders:

import { useState } from 'react';

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

Every time you click, count updates and the component re-renders with the new value.

The useState Hook

useState is React’s way to add state to function components:

const [value, setValue] = useState(initialValue);

Returns an array with two elements:

  1. Current value - the current state
  2. Setter function - function to update the state

Naming convention: [thing, setThing]

const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);

Basic useState Examples

Number:

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

String:

function NameInput() {
  const [name, setName] = useState('');
  
  return (
    <input 
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="Enter name"
    />
  );
}

Boolean:

function Toggle() {
  const [isOn, setIsOn] = useState(false);
  
  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

Array:

function TodoList() {
  const [todos, setTodos] = useState([]);
  
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text }]);
  };
  
  return (
    <div>
      <button onClick={() => addTodo('New todo')}>Add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

Object:

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  const updateName = (name) => {
    setUser({ ...user, name });
  };
  
  return (
    <input 
      value={user.name}
      onChange={(e) => updateName(e.target.value)}
    />
  );
}

Updating State

Replace the value:

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

setCount(5);  // Set to 5
setCount(count + 1);  // Increment

Functional update (when new state depends on old state):

// ⚠️ Can be unreliable
setCount(count + 1);

// ✅ Always reliable
setCount(prevCount => prevCount + 1);

Why functional updates matter:

function Counter() {
  const [count, setCount] = useState(0);
  
  const incrementThrice = () => {
    // Wrong - all use same count value
    setCount(count + 1);  // 0 + 1 = 1
    setCount(count + 1);  // 0 + 1 = 1
    setCount(count + 1);  // 0 + 1 = 1
    // Result: count is 1
    
    // Right - each uses previous update
    setCount(c => c + 1);  // 0 + 1 = 1
    setCount(c => c + 1);  // 1 + 1 = 2
    setCount(c => c + 1);  // 2 + 1 = 3
    // Result: count is 3
  };
  
  return <button onClick={incrementThrice}>{count}</button>;
}

Updating Objects

Never mutate state directly:

// ❌ Wrong - mutating state
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26;  // Don't do this!
setUser(user);  // Won't trigger re-render

// ✅ Right - create new object
setUser({ ...user, age: 26 });

// ✅ Right - functional update
setUser(prev => ({ ...prev, age: 26 }));

Updating nested objects:

const [user, setUser] = useState({
  name: 'Alice',
  address: {
    city: 'NYC',
    country: 'USA'
  }
});

// Update nested property
setUser({
  ...user,
  address: {
    ...user.address,
    city: 'LA'
  }
});

Updating Arrays

Add item:

const [items, setItems] = useState([1, 2, 3]);

// At end
setItems([...items, 4]);  // [1, 2, 3, 4]

// At beginning
setItems([0, ...items]);  // [0, 1, 2, 3]

// At specific position
const newItems = [...items];
newItems.splice(2, 0, 2.5);  // Insert at index 2
setItems(newItems);

Remove item:

// By index
setItems(items.filter((_, index) => index !== 2));

// By value
setItems(items.filter(item => item.id !== targetId));

Update item:

setItems(items.map(item => 
  item.id === targetId 
    ? { ...item, completed: true }
    : item
));

Replace array:

setItems([5, 6, 7]);

Multiple State Variables

Components can have multiple useState calls:

function UserProfile() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);
  const [isActive, setIsActive] = useState(true);
  const [preferences, setPreferences] = useState({});
  
  return (
    // Use all these state variables
    <div>...</div>
  );
}

Group related state:

// Instead of separate states
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

// Consider grouping
const [user, setUser] = useState({
  firstName: '',
  lastName: '',
  email: ''
});

State and Re-rendering

When state changes, React re-renders:

function Example() {
  const [count, setCount] = useState(0);
  
  console.log('Component rendered!');  // Logs on every render
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
// Click logs "Component rendered!" each time

State updates are batched:

function Example() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  const handleClick = () => {
    setCount(count + 1);  // Scheduled
    setFlag(!flag);       // Scheduled
    // Both updates batched into one re-render
  };
  
  return <button onClick={handleClick}>Update</button>;
}

Practical Examples

Example 1: Todo List

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  
  const addTodo = () => {
    if (input.trim()) {
      setTodos([
        ...todos,
        { id: Date.now(), text: input, completed: false }
      ]);
      setInput('');
    }
  };
  
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };
  
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <div>
      <input 
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add</button>
      
      <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={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Example 2: Form with Validation

function SignupForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    // Clear error for this field
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };
  
  const validate = () => {
    const newErrors = {};
    
    if (formData.username.length < 3) {
      newErrors.username = 'Username must be at least 3 characters';
    }
    
    if (!formData.email.includes('@')) {
      newErrors.email = 'Invalid email';
    }
    
    if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    
    return newErrors;
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    const newErrors = validate();
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }
    
    console.log('Form submitted:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input 
          name="username"
          value={formData.username}
          onChange={handleChange}
          placeholder="Username"
        />
        {errors.username && <span className="error">{errors.username}</span>}
      </div>
      
      <div>
        <input 
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      
      <div>
        <input 
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      
      <button type="submit">Sign Up</button>
    </form>
  );
}

Example 3: Counter with Limits

function LimitedCounter() {
  const [count, setCount] = useState(0);
  const [min] = useState(0);
  const [max] = useState(10);
  
  const increment = () => {
    setCount(c => Math.min(c + 1, max));
  };
  
  const decrement = () => {
    setCount(c => Math.max(c - 1, min));
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={decrement} disabled={count <= min}>
        -
      </button>
      <button onClick={reset}>Reset</button>
      <button onClick={increment} disabled={count >= max}>
        +
      </button>
      <p>Range: {min} - {max}</p>
    </div>
  );
}

Example 4: Modal Toggle

function ModalExample() {
  const [isOpen, setIsOpen] = useState(false);
  const [modalContent, setModalContent] = useState('');
  
  const openModal = (content) => {
    setModalContent(content);
    setIsOpen(true);
  };
  
  const closeModal = () => {
    setIsOpen(false);
  };
  
  return (
    <div>
      <button onClick={() => openModal('Welcome message!')}>
        Open Modal
      </button>
      
      {isOpen && (
        <div className="modal-overlay" onClick={closeModal}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <h2>{modalContent}</h2>
            <button onClick={closeModal}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
}

State Initialization

Simple value:

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

Computed initial value (runs once):

const [value, setValue] = useState(() => {
  const stored = localStorage.getItem('myValue');
  return stored ? JSON.parse(stored) : defaultValue;
});

Only use function form for expensive computations:

// Good - expensive operation runs once
const [data, setData] = useState(() => processLargeDataset());

// Bad - expensive operation runs every render
const [data, setData] = useState(processLargeDataset());

Common Patterns

1. Derived state (calculate from existing state):

function ShoppingCart() {
  const [items, setItems] = useState([]);
  
  // Don't store total in state - calculate it
  const total = items.reduce((sum, item) => sum + item.price, 0);
  const itemCount = items.length;
  
  return (
    <div>
      <p>Items: {itemCount}</p>
      <p>Total: ${total}</p>
    </div>
  );
}

2. Resetting state:

function Form() {
  const initialState = { name: '', email: '' };
  const [formData, setFormData] = useState(initialState);
  
  const reset = () => {
    setFormData(initialState);
  };
  
  return (
    <div>
      {/* form inputs */}
      <button onClick={reset}>Reset</button>
    </div>
  );
}

3. State based on previous state:

const [history, setHistory] = useState([]);

const addToHistory = (item) => {
  setHistory(prev => [...prev, item].slice(-10));  // Keep last 10
};

Best Practices

1. State should be minimal:

// Bad - storing derived data
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);  // Redundant!

// Good - calculate derived data
const [items, setItems] = useState([]);
const count = items.length;

2. Keep related state together:

// Bad
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// Good
const [position, setPosition] = useState({ x: 0, y: 0 });

3. Don’t mirror props in state unless needed:

// Bad - props change won't update state
function Component({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // If prop changes, state doesn't!
}

// Good - use prop directly
function Component({ count }) {
  return <div>{count}</div>;
}

// Good - only if you need to diverge from prop
function Component({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // State starts with prop but can change independently
}

Common Mistakes

1. Mutating state:

// Wrong
items.push(newItem);
setItems(items);

// Right
setItems([...items, newItem]);

2. Using stale state in async:

// Wrong - count might be stale
setTimeout(() => {
  setCount(count + 1);
}, 1000);

// Right - use functional update
setTimeout(() => {
  setCount(c => c + 1);
}, 1000);

3. Too much state:

// Bad - over-using state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');  // Redundant!

// Good - derive fullName
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;