Managing State with useState
Learn how to add interactivity to your components by managing state with the useState hook.
Topics Covered:
Prerequisites:
- Understanding Props
Video Tutorial
Overview
State allows components to create and manage their own data. When state changes, React re-renders the component to reflect the new data. State is what makes React components interactive - it enables components to 'remember' information and react to user interactions, API responses, and other changes over time. Unlike props (which come from parent components), state is internal to the component and can be updated using setter functions.
Understanding State vs Props
Understanding the difference between state and props is fundamental to React development. Props: • Passed from parent components • Read-only (immutable) • Used to configure components • Component cannot change its own props State: • Managed internally by component • Mutable (can be updated with setter) • Used for data that changes over time • Component controls its own state When to use each: • Use props for data that comes from parent or doesn't change • Use state for data that changes based on user interaction or events • Use state for form inputs, toggles, counters, API data after fetching
The useState Hook Basics
The useState hook lets you add state to functional components. It returns an array with the current state value and a function to update it. useState is the most commonly used React hook and is essential for building interactive components.
import { useState } from 'react';
// Basic usage
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>
);
}
// Multiple state variables
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Age"
/>
</form>
);
}useState(0) initializes state with 0. The array destructuring gives us `count` (current value) and `setCount` (updater function). You can have multiple useState calls in one component, each managing its own piece of state.
State Initialization Patterns
useState can accept an initial value directly or a function that returns the initial value. Using a function for initialization is important when the initial value is expensive to compute or when you want to ensure it's only calculated once.
// Direct initial value (calculated on every render - inefficient if expensive)
function Component1() {
const [data, setData] = useState(expensiveCalculation());
// ❌ expensiveCalculation() runs on every render!
}
// Function initializer (only runs once)
function Component2() {
const [data, setData] = useState(() => expensiveCalculation());
// ✅ expensiveCalculation() only runs on first render
}
// Example: Reading from localStorage
function Component3() {
// ❌ Wrong - reads on every render
const [value, setValue] = useState(
localStorage.getItem('key') || 'default'
);
// ✅ Correct - only reads once
const [value, setValue] = useState(() => {
const stored = localStorage.getItem('key');
return stored ? JSON.parse(stored) : 'default';
});
}
// Example: Complex initial state
function Component4() {
const [user, setUser] = useState(() => {
// This function only runs once, even if component re-renders
const defaultUser = {
name: 'Guest',
role: 'user',
preferences: { theme: 'light', notifications: true }
};
return defaultUser;
});
}Use the function form of useState when initializing with values that are expensive to compute or when reading from localStorage/sessionStorage. The function only runs once on mount, not on every re-render.
Updating State - Important Rules
Understanding how state updates work in React is crucial. There are important rules and patterns you must follow. Key Rules: • State updates are asynchronous • State updates are batched for performance • Always use the setter function, never modify state directly • State updates trigger re-renders • Multiple setState calls may be batched together
function Counter() {
const [count, setCount] = useState(0);
// ❌ WRONG - Direct mutation
const badIncrement = () => {
count = count + 1; // Don't do this! Won't trigger re-render
};
// ✅ CORRECT - Using setter
const goodIncrement = () => {
setCount(count + 1);
};
// ✅ BETTER - Functional update (when new state depends on old)
const betterIncrement = () => {
setCount(prevCount => prevCount + 1);
};
// Multiple updates in sequence
const handleMultipleUpdates = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// ❌ This will only increment once! (count is still 0 in all calls)
};
// ✅ Correct way - use functional updates
const handleMultipleUpdatesCorrect = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// ✅ This increments three times!
};
// Updating objects
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ WRONG - Mutating object directly
const badUpdate = () => {
user.age = 26; // Don't do this!
setUser(user); // React won't detect change
};
// ✅ CORRECT - Creating new object
const goodUpdate = () => {
setUser({ ...user, age: 26 }); // Spread operator
};
// ✅ OR functional update
const goodUpdate2 = () => {
setUser(prev => ({ ...prev, age: 26 }));
};
}Never mutate state directly. Always use setter functions. For updates that depend on previous state, use functional updates (prev => newValue). When updating objects/arrays, create new objects using spread operator or other immutable patterns.
State Updates and Re-renders
When you call a state setter function, React schedules a re-render. Understanding when and how re-renders happen helps you write performant code and avoid bugs.
function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
console.log('Component rendered'); // This logs on every render
// Setting state causes re-render
const increment = () => {
setCount(count + 1);
console.log(count); // Still shows old value! State update is async
};
// Multiple state updates may be batched
const handleClick = () => {
setName('Bob');
setCount(1);
// React may batch these into one re-render
};
// React 18: Automatic batching (even in async code)
const handleAsyncClick = async () => {
await fetch('/api/data');
setName('Charlie');
setCount(2);
// These are batched together!
};
return (
<div>
<p>{name}: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}State updates are asynchronous - you won't see the new value immediately after calling setState. React batches multiple state updates for performance. In React 18+, batching happens automatically even in async functions.
Common State Patterns
There are several common patterns for managing state in React components. Understanding these patterns helps you structure your components better.
// Pattern 1: Toggle state
function Toggle() {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(prev => !prev);
// OR
const toggle2 = () => setIsOn(!isOn);
return <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>;
}
// Pattern 2: Form state
function Form() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: 0
});
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<input
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
/>
);
}
// Pattern 3: Loading and error states
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await fetch('/api/data');
setData(await result.json());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{/* Render data */}</div>;
}
// Pattern 4: Derived state
function Cart() {
const [items, setItems] = useState([]);
// Derived values - no need for separate state
const total = items.reduce((sum, item) => sum + item.price, 0);
const itemCount = items.length;
return (
<div>
<p>{itemCount} items</p>
<p>Total: {total}</p>
</div>
);
}Common patterns include toggles, form state management, loading/error states, and derived values. Don't store values you can calculate from other state - use derived values instead.
When NOT to Use State
Not everything needs to be state! Using state unnecessarily can cause performance issues and bugs. Don't use state for: • Values that can be calculated from props or other state (derived values) • Values that don't affect rendering • Temporary variables used only in event handlers • Constants that don't change • Values that can be passed as props instead
// ❌ BAD - Storing derived value in state
function UserProfile({ firstName, lastName }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
}
// ✅ GOOD - Calculate directly
function UserProfile({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
// ❌ BAD - Unnecessary state
function Button() {
const [clicked, setClicked] = useState(false);
const handleClick = () => {
setClicked(true);
// Do something
};
}
// ✅ GOOD - Just use local variable
function Button() {
const handleClick = () => {
const clicked = true;
// Do something
};
}
// ❌ BAD - Constant in state
function Component() {
const [apiUrl, setApiUrl] = useState('https://api.example.com');
// Never changes, shouldn't be state
}
// ✅ GOOD - Use constant
const API_URL = 'https://api.example.com';
function Component() {
// Use API_URL directly
}Only use state for values that change over time AND affect what's rendered. Avoid storing derived values, constants, or temporary variables in state.
Rules of Hooks - useState Edition
useState follows the Rules of Hooks. Breaking these rules causes bugs and errors. Rules: • Only call hooks at the top level of your component • Don't call hooks inside loops, conditions, or nested functions • Only call hooks from React function components or custom hooks • Call hooks in the same order every render
// ❌ WRONG - Conditional hook call
function BadComponent({ shouldUseState }) {
if (shouldUseState) {
const [count, setCount] = useState(0); // ERROR!
}
return <div>...</div>;
}
// ✅ CORRECT - Always call hooks at top level
function GoodComponent({ shouldUseState }) {
const [count, setCount] = useState(0);
// Use conditionally if needed, but hook is always called
if (!shouldUseState) {
return <div>Static content</div>;
}
return <div>Count: {count}</div>;
}
// ❌ WRONG - Hook in loop
function BadList() {
const items = ['a', 'b', 'c'];
return items.map(item => {
const [value, setValue] = useState(item); // ERROR!
return <div>{value}</div>;
});
}
// ✅ CORRECT - Extract to component
function ListItem({ item }) {
const [value, setValue] = useState(item);
return <div>{value}</div>;
}
function GoodList() {
const items = ['a', 'b', 'c'];
return items.map(item => <ListItem key={item} item={item} />);
}Hooks must be called in the same order every render. This allows React to track which state belongs to which hook. Conditional calls break this requirement.
Conclusion
State is essential for interactive components. Remember: only call useState at the top level, never inside loops or conditions.