State & useState Hook
State is what makes React components interactive. It’s data that changes over time and triggers re-renders when updated.
State vs Props
Understanding the difference is crucial:
Props State Passed from parent Managed within component Read-only (immutable) Can be updated Received as function parameters Created with useState Changes come from parent Changes come from component itself
┌─────────────────────────────────────────────────────────────────┐
│ Parent Component │
│ │
│ ┌─────────────────────┐ │
│ │ state = { count } │ │
│ └─────────────────────┘ │
│ │ │
│ passes as prop │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Child Component │ │
│ │ props = { count: 5 } │ │
│ │ (read-only) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The useState Hook
Hooks are functions that let you “hook into” React features. useState is the fundamental hook for managing state.
Basic Syntax
import { useState } from 'react' ;
const [ stateValue , setStateFunction ] = useState ( initialValue );
stateValue : Current value of the state
setStateFunction : Function to update the state
initialValue : Starting value (any type: string, number, boolean, object, array)
Simple Counter Example
import { useState } from 'react' ;
function Counter () {
const [ count , setCount ] = useState ( 0 );
return (
< div >
< p > Count: { count } </ p >
< button onClick = { () => setCount ( count + 1 ) } > + </ button >
< button onClick = { () => setCount ( count - 1 ) } > - </ button >
< button onClick = { () => setCount ( 0 ) } > Reset </ button >
</ div >
);
}
Toggle Example
function ToggleButton () {
const [ isOn , setIsOn ] = useState ( false );
return (
< button
onClick = { () => setIsOn ( ! isOn ) }
style = { { backgroundColor: isOn ? 'green' : 'gray' } }
>
{ isOn ? 'ON' : 'OFF' }
</ button >
);
}
Rules of useState
1. Never Modify State Directly
React won’t know the state changed and won’t re-render.
// ❌ WRONG - Direct mutation
count = count + 1 ;
// ✅ CORRECT - Use the setter function
setCount ( count + 1 );
2. Call Hooks at the Top Level
Don’t call hooks inside loops, conditions, or nested functions.
function BadComponent () {
// ❌ WRONG - Hook inside condition
if ( someCondition ) {
const [ value , setValue ] = useState ( 0 );
}
}
function GoodComponent () {
// ✅ CORRECT - Hook at top level
const [ value , setValue ] = useState ( 0 );
// Use conditions with the state value, not the hook
if ( someCondition ) {
// do something with value
}
}
3. Only Call Hooks in React Functions
Hooks only work in React function components or custom hooks.
Functional Updates (Previous State)
When your new state depends on the previous state, use the functional update form:
// ❌ Can cause bugs with multiple rapid updates
setCount ( count + 1 );
setCount ( count + 1 ); // Both use the same stale 'count'
// ✅ CORRECT - Uses the latest state
setCount ( prevCount => prevCount + 1 );
setCount ( prevCount => prevCount + 1 ); // Each gets the updated value
Why does this matter? React batches state updates for performance. When you click a button that triggers multiple setCount calls, they may all read the same initial value of count.
Practical Example
function BatchingDemo () {
const [ count , setCount ] = useState ( 0 );
const handleClick = () => {
// All three will use functional updates for correct behavior
setCount ( prev => prev + 1 );
setCount ( prev => prev + 1 );
setCount ( prev => prev + 1 );
// Result: count increments by 3
};
return < button onClick = { handleClick } > Add 3 (count: { count } ) </ button > ;
}
Lazy Initialization
If your initial state requires expensive computation, pass a function:
// ❌ Expensive function runs on EVERY render
const [ data , setData ] = useState ( expensiveComputation ());
// ✅ Function only runs on initial render
const [ data , setData ] = useState (() => expensiveComputation ());
Real-World Example
function TodoApp () {
// Load todos from localStorage only on first render
const [ todos , setTodos ] = useState (() => {
const saved = localStorage . getItem ( 'todos' );
return saved ? JSON . parse ( saved ) : [];
});
// ...
}
State with Objects
When state is an object, you must spread the previous state to avoid losing properties.
function UserForm () {
const [ user , setUser ] = useState ({
name: '' ,
email: '' ,
age: 0
});
const updateName = ( newName ) => {
// ❌ WRONG - loses email and age
setUser ({ name: newName });
// ✅ CORRECT - spread previous state
setUser ({ ... user , name: newName });
// ✅ ALSO CORRECT - functional update
setUser ( prev => ({ ... prev , name: newName }));
};
return (
< form >
< input
value = { user . name }
onChange = { ( e ) => setUser ({ ... user , name: e . target . value }) }
placeholder = "Name"
/>
< input
value = { user . email }
onChange = { ( e ) => setUser ({ ... user , email: e . target . value }) }
placeholder = "Email"
/>
</ form >
);
}
Nested Objects
For deeply nested objects, consider flattening your state or using a library like Immer.
const [ user , setUser ] = useState ({
name: 'Alice' ,
address: {
city: 'NYC' ,
zip: '10001'
}
});
// Updating nested property
setUser ( prev => ({
... prev ,
address: {
... prev . address ,
city: 'LA'
}
}));
State with Arrays
Arrays require special handling because they’re reference types.
function TodoList () {
const [ todos , setTodos ] = useState ([
{ id: 1 , text: 'Learn React' , done: false },
{ id: 2 , text: 'Build project' , done: false }
]);
// ✅ Add item
const addTodo = ( text ) => {
setTodos ([ ... todos , { id: Date . now (), text , done: false }]);
};
// ✅ Remove item
const removeTodo = ( id ) => {
setTodos ( todos . filter ( todo => todo . id !== id ));
};
// ✅ Update item
const toggleTodo = ( id ) => {
setTodos ( todos . map ( todo =>
todo . id === id ? { ... todo , done: ! todo . done } : todo
));
};
// ✅ Insert at specific position
const insertAt = ( index , newTodo ) => {
setTodos ([
... todos . slice ( 0 , index ),
newTodo ,
... todos . slice ( index )
]);
};
return (
< ul >
{ todos . map ( todo => (
< li key = { todo . id } >
< input
type = "checkbox"
checked = { todo . done }
onChange = { () => toggleTodo ( todo . id ) }
/>
< span style = { { textDecoration: todo . done ? 'line-through' : 'none' } } >
{ todo . text }
</ span >
< button onClick = { () => removeTodo ( todo . id ) } > Delete </ button >
</ li >
)) }
</ ul >
);
}
Never mutate arrays directly! Methods like push, pop, splice, sort, and reverse mutate the original array.// ❌ WRONG - mutates original array
todos . push ( newTodo );
setTodos ( todos );
// ✅ CORRECT - creates new array
setTodos ([ ... todos , newTodo ]);
Array Operation Cheat Sheet
Operation Mutating (Don’t Use) Non-Mutating (Use This) Add push, unshift[...arr, item], [item, ...arr]Remove pop, shift, splicefilter()Replace arr[i] = x, splicemap()Sort sort()[...arr].sort()Reverse reverse()[...arr].reverse()
Multiple State Variables
You can (and often should) use multiple useState calls:
function UserProfile () {
// Separate concerns into individual state pieces
const [ name , setName ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
const [ isSubscribed , setIsSubscribed ] = useState ( false );
const [ notifications , setNotifications ] = useState ([]);
// Each can be updated independently
}
When to Use Object State vs Multiple States
// ✅ Multiple states - when values change independently
const [ firstName , setFirstName ] = useState ( '' );
const [ lastName , setLastName ] = useState ( '' );
// ✅ Object state - when values always change together
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
// ✅ Object state - for forms with many fields
const [ formData , setFormData ] = useState ({
username: '' ,
email: '' ,
password: '' ,
confirmPassword: ''
});
State Updates and Re-rendering
When state changes, React:
Schedules a re-render
Calls your component function again
Compares the new JSX with the previous
Updates only what changed in the DOM
function RenderDemo () {
const [ count , setCount ] = useState ( 0 );
console . log ( 'Component rendered!' ); // Logs on every render
return (
< div >
< p > Count: { count } </ p >
< button onClick = { () => setCount ( count + 1 ) } > Increment </ button >
</ div >
);
}
State updates are asynchronous. The new value isn’t immediately available after calling the setter.const [ count , setCount ] = useState ( 0 );
const handleClick = () => {
setCount ( count + 1 );
console . log ( count ); // Still logs 0, not 1!
};
🎯 Practice Exercises
Exercise 1: Shopping Cart Item Counter
Exercise 2: Expandable FAQ
Build an FAQ item that expands/collapses: function FAQItem ({ question , answer }) {
const [ isOpen , setIsOpen ] = useState ( false );
return (
< div className = "faq-item" >
< button
onClick = { () => setIsOpen ( ! isOpen ) }
className = "faq-question"
>
{ question }
< span > { isOpen ? '−' : '+' } </ span >
</ button >
{ isOpen && (
< div className = "faq-answer" >
{ answer }
</ div >
) }
</ div >
);
}
// Usage
< FAQItem
question = "What is React?"
answer = "React is a JavaScript library for building user interfaces."
/>
Exercise 3: Multi-Step Form
Summary
Concept Description useStateHook to add state to functional components Setter Function Only way to update state and trigger re-render Functional Updates setState(prev => newValue) for updates based on previous stateLazy Initialization useState(() => expensiveCalc()) for expensive initial valuesImmutability Never mutate state directly; always create new values Objects Spread previous state: { ...prev, newProp } Arrays Use map, filter, spread; never push, pop, splice Batching React batches multiple state updates for performance
Next Steps In the next chapter, you’ll learn about Handling Events — responding to user clicks, form submissions, and keyboard input!