Introduction
State is what makes React components interactive. While props are data passed in from a parent, state is data that a component owns and can change over time. The useState hook is the primary way to add state to function components. Every time state changes, React re-renders the component to reflect the new data.
Key Concepts
- State Variable: A value that persists across re-renders and whose changes trigger a new render.
- Setter Function: The function returned by
useStatethat lets you update the state value. - Initial Value: The argument passed to
useState, used only on the first render. - Re-Render: When state changes, React calls your component function again with the new state value.
Real World Context
Consider a shopping cart. The number of items, the selected shipping method, and whether the promo code input is visible are all state. Each time the user adds an item or toggles a section, the relevant state changes and React re-renders the affected parts of the UI. Without state, the UI would be static and unresponsive to user actions.
Deep Dive
Basic Syntax
tsximport { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(0)}>Reset</button> </div> ); }
useState(0) returns a pair: the current value (count) and a setter (setCount). The initial value 0 is used only on the first render.
Multiple State Variables
You can call useState multiple times for independent pieces of state:
tsxfunction SignupForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [agreedToTerms, setAgreedToTerms] = useState(false); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!agreedToTerms) return; registerUser(email, password); }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <label> <input type="checkbox" checked={agreedToTerms} onChange={e => setAgreedToTerms(e.target.checked)} /> I agree to the terms </label> <button type="submit">Sign Up</button> </form> ); }
Lazy Initialization
If computing the initial value is expensive, pass a function to avoid recalculating on every render:
tsx// Expensive: runs JSON.parse on every render (result discarded after first) const [data, setData] = useState(JSON.parse(localStorage.getItem('data') || '{}')); // Lazy: runs JSON.parse only on the first render const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data') || '{}'));
Rules of Hooks
useState (and all hooks) must follow two rules:
- Call at the top level — never inside loops, conditions, or nested functions.
- Call only in React functions — component functions or custom hooks.
Common Pitfalls
- Expecting state to update immediately — State updates are asynchronous. Logging
countright aftersetCount(count + 1)will show the old value. The new value is available on the next render. - Using state for derived data — If a value can be computed from props or other state, do not store it in state. Compute it during render instead:
const fullName = firstName + ' ' + lastName;.
Best Practices
- Keep state minimal — Only store the source of truth. Derive everything else during render. This eliminates bugs caused by state getting out of sync.
- Use multiple useState calls for unrelated values — Grouping unrelated values into a single object makes updates verbose. Keep independent values in separate state variables.
Summary
useStatereturns a[value, setter]pair that persists across renders and triggers re-renders on change.- Pass a function to
useStatefor expensive initial value computations (lazy initialization). - Always call hooks at the top level of your component, never inside conditions or loops.
Code Examples
import { useState } from 'react';
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
// Derived value — no need for separate state
const fahrenheit = (celsius * 9) / 5 + 32;
return (
<div>
<label>
Celsius:
<input
type="number"
value={celsius}
onChange={e => setCelsius(Number(e.target.value))}
/>
</label>
<p>{celsius}°C = {fahrenheit.toFixed(1)}°F</p>
</div>
);
}