Module 8 β React State & Props
Give your components memory β make them respond to user actions and update the UI automatically.
Overview π
State is data that a component owns and can change over time. When state changes, React re-renders the component automatically. This module covers useState, controlled inputs, event handling in React, and how to share state between components by lifting it up to a common parent and passing it down through props.
Why This Matters π‘
Without state, React components are just templates. State is what makes a button a toggle, a form interactive, and a counter count. Understanding how state flows β down through props, up through callbacks β is the key mental model for building any non-trivial React application.
Learning Goals π―
By the end of this module you should be able to:
- Use
useStateto add local state to a component - Update state in response to events
- Build controlled form inputs
- Lift state to a parent component and pass it down as props
- Pass callback functions as props to allow children to update parent state
- Use
useEffectto run code after a component renders
Vocabulary π
| Term | Definition |
|---|---|
| State | Data owned by a component that can change, triggering a re-render |
useState | A React hook for adding state to a function component |
| Re-render | React re-running a component function and updating the DOM |
| Controlled input | A form input whose value is bound to state |
| Lifting state | Moving state to a parent so multiple children can share it |
| Props drilling | Passing props through multiple layers of components |
useEffect | A hook that runs side effects after each render (or on specific changes) |
| Dependency array | The second argument to useEffect β controls when it runs |
Core Concepts π§
useState
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0) // [current value, updater function]
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>increment</button>
<button onClick={() => setCount(0)}>reset</button>
</div>
)
}
Controlled inputs
import { useState } from 'react'
export default function SearchBar() {
const [query, setQuery] = useState('')
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="search..."
/>
)
}
Lifting state
// parent owns the state
export default function App() {
const [selected, setSelected] = useState(null)
return (
<>
<ItemList onSelect={setSelected} />
<ItemDetail item={selected} />
</>
)
}
// child calls the parent's callback
function ItemList({ onSelect }) {
const items = ['html', 'css', 'javascript']
return (
<ul>
{items.map((item) => (
<li key={item} onClick={() => onSelect(item)}>{item}</li>
))}
</ul>
)
}
useEffect
import { useState, useEffect } from 'react'
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
// runs after render, whenever userid changes
const load = async () => {
const res = await fetch(`https://api.example.com/users/${userId}`)
const data = await res.json()
setUser(data)
}
load()
}, [userId]) // dependency array β re-runs when userid changes
if (!user) return <p>loading...</p>
return <p>{user.name}</p>
}
Examples π»
A form with multiple controlled inputs:
import { useState } from 'react'
export default function ProfileForm() {
const [form, setForm] = useState({ name: '', email: '' })
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value })
}
const handleSubmit = (e) => {
e.preventDefault()
console.log('submitted:', form)
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="name" />
<input name="email" value={form.email} onChange={handleChange} placeholder="email" />
<button type="submit">save</button>
</form>
)
}
Fetching on mount with useEffect:
import { useState, useEffect } from 'react'
export default function PostList() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
.then((res) => res.json())
.then((data) => setPosts(data))
}, []) // empty array β runs once on mount
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Common Mistakes β οΈ
- Mutating state directly. Never do
state.push(item)orstate.name = 'new'. Always call the setter with a new value:setState([...state, item]). - Using stale state in event handlers. If your new state depends on the previous value, use the updater form:
setCount((prev) => prev + 1). - Forgetting the dependency array in
useEffect. Without it, the effect runs on every render. An empty[]means run once on mount. List the specific values that should trigger re-runs. - Setting state in a
useEffectwith no dependencies. This causes an infinite loop: render β effect β setState β re-render β effect β ... - Not using controlled inputs. Uncontrolled inputs (no
valueprop) make it hard to validate, reset, or pre-fill forms in React.
Debugging Tips π
- React DevTools shows the current state and props for every component β this is the fastest way to verify state is what you expect.
- If an update does not seem to work, check that you are calling the setter function, not the value:
setCount(...)notcount(...). - If a
useEffectis running too often, addconsole.loginside to trace when it triggers, then review the dependency array. - If state seems one render behind, remember: state updates are asynchronous. Log inside the component body, not after the
setStatecall.
Exercise ποΈ
The exercise for this module is in the class repository:
ttpr-lagcc-spring-2026 β Module 8 Exercise (opens in new tab)
Extend your PokΓ©mon app from Module 6 using React. Add a search input with controlled state, fetch PokΓ©mon data on form submit using useEffect, display the result, and handle loading and error states with separate state variables.
Additional Resources π
- React docs β State: A Component's Memory (opens in new tab) β official useState guide
- React docs β Sharing State Between Components (opens in new tab) β lifting state up
- React docs β Synchronizing with Effects (opens in new tab) β useEffect in depth
- React docs β You Might Not Need an Effect (opens in new tab) β important: when not to use useEffect
- javascript.info β React (opens in new tab) β supplemental reading on async patterns
Recap Checklist βοΈ
- I can add state to a component with
useState - I update state using the setter, never by mutating directly
- I can build a controlled form input
- I understand how to lift state to a parent and pass it down
- I can pass a callback function as a prop so a child can update parent state
- I can use
useEffectto fetch data when a component mounts - I understand the dependency array and what happens without it