React: State Basics: useState
Last updated: 2026-08-26
State is a component’s memory. If Props are parameters given to you by others (read-only), then State is your own little notebook (editable). useState is the “memory chip” that React installs in a component.
1. What You'll Learn
- What is State, and why is State needed?
- Basic Usage of useState
- The Correct Way to Update Array and Object States (Immutability)
- Managing Multiple State Variables
- Lifting State Up
2. Reflections Triggered by a Counter
(1) Pain Point: Regular variables do not trigger UI updates
Bob wants to make a simple counter: when you click the button, the number increases by 1.
// ❌ Ordinary Variables,Will not trigger a re-render
function Counter() {
let count = 0 // Ordinary Variables
function handleClick() {
count = count + 1 // Variable changed, but UI won't update!
console.log(count) // Console Display 1, 2, 3... But the page always displays 0
}
return (
<div>
<p>Count:{count}</p> {/* Always Show 0 */}
<button onClick={handleClick}>+1</button>
</div>
)
}
Bob discovered a problem: Ordinary variables do not notify React to update the UI. Every time the component re-renders, count is reset to 0.
(2) A Solution Using useState
// ✅ Usage State,React It automatically tracks changes and updates UI
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // Initial value 0
function handleClick() {
setCount(count + 1) // Update Status → React Automatic Re-rendering
}
return (
<div>
<p>Count:{count}</p> {/* Automatically update with each click */}
<button onClick={handleClick}>+1</button>
</div>
)
}
Return Value: useState returns an array [current value, setter function]. After calling setCount, React automatically re-renders the component to display the latest values.
3. Core Concepts of useState
(1) Deconstructing Assignment
import { useState } from 'react'
// useState(initialValue) Back [value, setValue]
const [count, setCount] = useState(0)
// ^ ^ ^
// | | └─ Initial value(Applies only to the first render)
// | └─ Update Function(Triggers a re-render after being called)
// └─ Current status value(Get the latest value on every render)
| Section | Description |
|---|---|
count |
Current state value; the latest value is retrieved with each render |
setCount |
Update function: Calling it → React re-renders the component |
useState(0) |
Initial value (used during the first render; ignored in subsequent renders) |
(2) Update Mechanism
function Example() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1) // 1. Request Update
// Note:Here count Still the old value!
console.log(count) // Output 0,No 1
}
return <button onClick={handleClick}>{count}</button>
}
Key Concept:
setCount(count + 1)means "ask React to updatecounton the next render," not "immediately changecount." This is called asynchronous updating.
(3) Functional Updates
If the new state depends on the old state, you should use functional updates:
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// ✅ Recommendations:Functional Updates
setCount(prev => prev + 1)
setCount(prev => prev + 1) // Called twice in a row, count becomes +2
}
// ❌ Not recommended:Update Directly(Chain calls take effect only once)
function handleBadClick() {
setCount(count + 1)
setCount(count + 1) // Called twice count All are old values,The result was only +1
}
return <button onClick={handleClick}>Currently:{count}</button>
}
| Method | Syntax | Characteristics |
|---|---|---|
| Update Directly | setCount(count + 1) |
Simple, but data is lost with consecutive calls |
| Functional Update | setCount(prev => prev + 1) |
Accurate; works correctly even with consecutive calls |
4. Updating Arrays and Objects (Immutability)
React requires that state not be modified directly. Never use state.push() or state.name = 'xxx'; instead, create a new array or object to replace the old value.
(1) Objects to Update
function UserEditor() {
const [user, setUser] = useState({
name: 'Alice',
age: 28,
email: 'alice@example.com'
})
function updateName(newName) {
// ❌ Error:Edit directly state Object
user.name = newName
// React Changes will not be detected,UI Will not be updated
// ✅ Correct:Create a new object
setUser({ ...user, name: newName })
}
function updateAge(newAge) {
// ✅ Expand Operator:Keep the other fields,Update Only age
setUser({ ...user, age: newAge })
}
function resetUser() {
// ✅ Reset to default values
setUser({ name: '', age: 0, email: '' })
}
return (
<div>
<p>{user.name} - {user.age} years old</p>
<button onClick={() => updateName('Bob')}>Change Name Bob</button>
<button onClick={() => updateAge(user.age + 1)}>Age +1</button>
</div>
)
}
(2) Update the array
function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: 'Apple', qty: 2 },
{ id: 2, name: 'Banana', qty: 1 }
])
// Add:Create a new array using the spread operator
function addItem(name) {
setItems([...items, { id: Date.now(), name, qty: 1 }])
}
// Delete:use filter Create a new array
function removeItem(id) {
setItems(items.filter(item => item.id !== id))
}
// Update:use map Create a new array
function updateQty(id, newQty) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: newQty } : item
))
}
return (
<div>
<button onClick={() => addItem('Orange')}>Add orange</button>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} × {item.qty}
<button onClick={() => updateQty(item.id, item.qty + 1)}>+</button>
<button onClick={() => removeItem(item.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
▶ Example: Array Operations Quick Reference Table
Output:
State: arr
// ============================================
// Example:React Array Operations in C 6 Common Scenarios
// ============================================
const [arr, setArr] = useState([1, 2, 3])
// 1. Append to the end
setArr([...arr, 4]) // [1, 2, 3, 4]
// 2. Add to the beginning
setArr([0, ...arr]) // [0, 1, 2, 3]
// 3. Insert in the middle
const insertAt = 1
setArr([...arr.slice(0, insertAt), 99, ...arr.slice(insertAt)])
// [1, 99, 2, 3]
// 4. Delete Element(filter)
setArr(arr.filter(n => n !== 2)) // [1, 3]
// 5. Update Element(map)
setArr(arr.map(n => n === 2 ? 22 : n)) // [1, 22, 3]
// 6. Sort(Copy first, then sort)
setArr([...arr].sort((a, b) => b - a)) // [3, 2, 1]
Output:
Array state operations: push → add item, filter → remove item, map → update item. Each returns a new array reference for React re-rendering
5. Multiple State Variables
A component can have multiple state variables. It is recommended to break them down by logic rather than cramming them into a single large object:
function RegistrationForm() {
// ✅ Recommendations:Logical Breakdown
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState({})
// ❌ Not recommended:All states are stored in a single object
const [form, setForm] = useState({
name: '', email: '', password: '',
isSubmitting: false, errors: {}
})
// Expand the entire object when updating,Fields that are easy to overlook
}
| Strategy | Advantages | Disadvantages |
|---|---|---|
Split into multiple useStates |
Precise updates, type-safe, and easy to read | Too many variables when there's a lot of state |
Combined into a single useState |
Convenient for updating multiple fields at once | The update syntax is cumbersome and it's easy to forget to expand it |
Recommendation: Use multiple
useStateinstances for independent states (such as input fields, loading indicators, and error messages); logically related fields (such as all fields in a user profile) can be combined into a single objeto.
6. Lifting State Up
When multiple components need to share the same state, lift the state up to their nearest common parent component.
graph TB
subgraph "Before the status upgrade(Data is not shared)"
A[Parent Component App] --- B[Child component A<br/>Have their own count]
A --- C[Child component B<br/>Have their own count]
end
subgraph "After the status upgrade(Data Sharing)"
D[Parent Component App<br/>**count Here**] --- E[Child component A<br/>Read count, Call setCount]
D --- F[Child component B<br/>Read count, Call setCount]
end
▶ Example: Practical Application of State Promotion
Output:
State: arr (setter: setArr)
// ============================================
// Example:Temperature Converter(Status Upgrade)
// Features:Converting Between Celsius and Fahrenheit,Share the same temperature value
// ============================================
// ---- Child component:Celsius Temperature Input ----
function CelsiusInput({ celsius, onCelsiusChange }) {
return (
<div>
<label>Celsius(°C):</label>
<input
value={celsius}
onChange={e => onCelsiusChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- Child component:Fahrenheit Temperature Input ----
function FahrenheitInput({ fahrenheit, onFahrenheitChange }) {
return (
<div>
<label>Fahrenheit(°F):</label>
<input
value={fahrenheit}
onChange={e => onFahrenheitChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- Parent Component:Status is managed here ----
function TemperatureConverter() {
// State Propagated to the Common Parent Component
const [temperature, setTemperature] = useState('')
function handleCelsiusChange(value) {
setTemperature(value) // Store at Celsius
// There is no need for two states!Keep only one,Another one calculated using a formula
}
function handleFahrenheitChange(value) {
// Fahrenheit → Celsius:°C = (°F - 32) × 5/9
setTemperature(value ? ((parseFloat(value) - 32) * 5 / 9).toFixed(1) : '')
}
const celsius = temperature
const fahrenheit = temperature
? (parseFloat(temperature) * 9 / 5 + 32).toFixed(1)
: ''
return (
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<h2>Temperature Converter</h2>
<CelsiusInput celsius={celsius} onCelsiusChange={handleCelsiusChange} />
<FahrenheitInput fahrenheit={fahrenheit} onFahrenheitChange={handleFahrenheitChange} />
{temperature && (
<p style={{ color: '#666', marginTop: '12px' }}>
{celsius}°C = {fahrenheit}°F
</p>
)}
</div>
)
}
// Enter in the Celsius input field 100 → Automatic Display in Fahrenheit 212°F
// Enter in the Fahrenheit input field 212 → Automatic Celsius Display 100°C
Output:
Enter 100 in Celsius → Fahrenheit auto-shows 212.0. Displays "100°C = 212.0°F". Two inputs stay synchronized.
7. Complete Example: Shopping Cart Counter
// ============================================
// Complete Example:Shopping Cart(useState Comprehensive Application)
// Features:Add Item、Increase or Decrease Quantity、Delete、Total Price Calculation
// ============================================
import { useState } from 'react'
function ShoppingCart() {
// Several State Variable,Logical Breakdown
const [items, setItems] = useState([
{ id: 1, name: 'React Hands-On Tutorials', price: 89, qty: 1 },
{ id: 2, name: 'TypeScript Getting Started', price: 59, qty: 2 }
])
const [discountCode, setDiscountCode] = useState('')
const [appliedDiscount, setAppliedDiscount] = useState(0)
// Increase the quantity(Functional updates ensure accuracy)
function increment(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: item.qty + 1 } : item
))
}
// Decrease the quantity(No less than 1)
function decrement(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: Math.max(1, item.qty - 1) } : item
))
}
// Delete Product
function remove(id) {
setItems(items.filter(item => item.id !== id))
}
// Apply a discount code
function applyDiscount() {
if (discountCode === 'REACT2026') {
setAppliedDiscount(20) // over 100 minus 20
} else {
alert('The discount code is invalid.')
}
}
// Calculate the Total Price
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
const total = Math.max(0, subtotal - appliedDiscount)
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2>🛒 Shopping Cart</h2>
{/* Product List */}
{items.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center', padding: '40px' }}>
Your shopping cart is empty,Go check it out!!
</p>
) : (
items.map(item => (
<div key={item.id} style={{
display: 'flex', alignItems: 'center',
padding: '12px', borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ flex: 1 }}>
<h4 style={{ margin: 0 }}>{item.name}</h4>
<p style={{ margin: '4px 0', color: '#ff4d4f' }}>${item.price}</p>
</div>
{/* Quantity Control */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button onClick={() => decrement(item.id)} style={btnStyle}>−</button>
<span>{item.qty}</span>
<button onClick={() => increment(item.id)} style={btnStyle}>+</button>
</div>
{/* Subtotal */}
<p style={{ margin: '0 16px', fontWeight: 'bold', width: '80px', textAlign: 'right' }}>
${item.price * item.qty}
</p>
{/* Delete */}
<button onClick={() => remove(item.id)} style={{ ...btnStyle, backgroundColor: '#ff4d4f', color: 'white' }}>
✕
</button>
</div>
))
)}
{/* Discount Code */}
<div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
<input
value={discountCode}
onChange={e => setDiscountCode(e.target.value)}
placeholder="Enter the discount code"
style={{ flex: 1, padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
<button onClick={applyDiscount} style={{
padding: '8px 16px', backgroundColor: '#52c41a', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Applications
</button>
</div>
{/* Price Summary */}
<div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#fafafa', borderRadius: '8px' }}>
<p>Subtotal:${subtotal}</p>
{appliedDiscount > 0 && <p style={{ color: '#52c41a' }}>Discount: -${appliedDiscount}</p>}
<p style={{ fontSize: '20px', fontWeight: 'bold' }}>Total:${total}</p>
</div>
</div>
)
}
const btnStyle = {
width: '32px', height: '32px',
border: '1px solid #d9d9d9', borderRadius: '4px',
backgroundColor: 'white', cursor: 'pointer',
fontSize: '16px', display: 'flex', alignItems: 'center',
justifyContent: 'center'
}
Interaction Flow:
- Tap +/− to adjust the quantity; the subtotal and total are updated in real time
- Click ✕ to delete an item; display a friendly prompt when the shopping cart is empty
- Enter the promo code
REACT2026to save 20 yuan
▶ Example 3: Updating an Object's State—Editing User Information
Output:
Subheading: "Temperature Converter". Displays: "Celsius(°C):". State: temperature (setter: setTemperature)
function UserEditor() {
const [user, setUser] = useState({ name: 'Alice', email: 'alice@test.com', age: 28 })
function updateField(field, value) {
setUser(prev => ({ ...prev, [field]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Edit Profile</h3>
<input value={user.name} onChange={e => updateField('name', e.target.value)}
placeholder="Name" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input value={user.email} onChange={e => updateField('email', e.target.value)}
placeholder="Email" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input type="number" value={user.age} onChange={e => updateField('age', Number(e.target.value))}
placeholder="Age" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 13 }}>
{JSON.stringify(user, null, 2)}
</pre>
</div>
)
}
Output:
State: address. Inputs: Street, City, ZIP
▶ Example 4: Nested Object States—Address Management
Output:
Displays: "Edit Profile". State: user (setter: setUser). Input: Name, Email, Age
function AddressForm() {
const [address, setAddress] = useState({
street: '', city: '', zip: '',
country: 'US', isPrimary: true,
})
function update(path, value) {
setAddress(prev => ({ ...prev, [path]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Shipping Address</h3>
<input value={address.street} onChange={e => update('street', e.target.value)}
placeholder="Street" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={address.city} onChange={e => update('city', e.target.value)}
placeholder="City" style={{ flex: 2, padding: 8, borderRadius: 4 }} />
<input value={address.zip} onChange={e => update('zip', e.target.value)}
placeholder="ZIP" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<select value={address.country} onChange={e => update('country', e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="JP">Japan</option>
</select>
<label>
<input type="checkbox" checked={address.isPrimary}
onChange={e => update('isPrimary', e.target.checked)} /> Primary address
</label>
</div>
)
}
Output:
Shipping form: Street, City, ZIP, Country dropdown (US/CN/JP), "Primary address" checkbox. All fields update state reactively.
▶ Example 5: Functional Updates Solve the Problem of Batch Updates
Output:
Displays: "Shipping Address". State: address (setter: setAddress). Input: Street, City, ZIP. Dropdown: United States, China, Japan
function ScoreBoard() {
const [score, setScore] = useState(0)
const [multiplier, setMultiplier] = useState(1)
function addPoints(base) {
setScore(prev => prev + base * multiplier)
}
function resetScore() {
setScore(0)
setMultiplier(1)
}
function doubleMultiplier() {
setMultiplier(prev => Math.min(prev * 2, 8))
}
return (
<div style={{ maxWidth: 300, margin: '0 auto', textAlign: 'center' }}>
<h3>Score: {score}</h3>
<p>Multiplier: x{multiplier}</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', marginBottom: 8 }}>
<button onClick={() => addPoints(10)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+10 pts</button>
<button onClick={() => addPoints(50)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+50 pts</button>
<button onClick={() => addPoints(100)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+100 pts</button>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
<button onClick={doubleMultiplier} style={{ padding: '8px 16px', cursor: 'pointer' }}>2x Multiplier</button>
<button onClick={resetScore} style={{ padding: '8px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</div>
)
}
Output:
Score: 0, Multiplier: x1. +10/+50/+100 add points×multiplier. "2x Multiplier" doubles (max x8). Reset → Score: 0, x1
❓ FAQ
useState and regular variables?useState incurs additional overhead (tracking changes, scheduling rendering), so it should only be used in scenarios where "UI updates are triggered when changes occur." If a variable changes but does not require a UI update (such as a timer ID or scroll position), using useRef (Lesson 10) is more appropriate.useStates can there be in a single component? Are there any performance issues?useStates based on logic rather than using a single large object. Each useState call is a "state unit," and React can efficiently handle a large number of useStates. Common components typically have 3–8 useStates.setState update synchronous or asynchronous?setState was "automatically batched" within event handlers, but synchronous within setTimeout and Promise calls. React 18 introduced Automatic Batching, so setState is now batch-asynchronous regardless of whether it’s called within an event handler, setTimeout, Promise, or fetch callback. If you absolutely need to access the updated DOM synchronously, you can use flushSync(() => setState(...)).📖 Summary
- State is a component's "memory"; when it changes, it automatically triggers a UI update
useState(initialValue)Returns the[value, setValue]array- Immutability: Never modify the state directly; instead, create a new object or array to replace it.
- Functional update
setCount(prev => prev + 1)is more accurate when relying on old values - State Elevation: When multiple child components need to share data, the state is elevated to a common parent component.
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create a
LikeButtoncomponent that toggles between ❤️ and 🤍 states when clicked and displays the number of likes. - Advanced Exercise (Difficulty ⭐⭐): Create a
ExpenseTrackercomponent that usesuseStateto manage a list of expenses (description, amount, date), and supports adding and removing items. - Challenge (Difficulty: ⭐⭐⭐): Create a
ColorPickercomponent that uses threeuseStateinstances to manage the R, G, and B slider values (0–255), and displays a real-time color preview and hex color code.