The State
The State
Props pass data down, but nothing so far can change while the app is running. Click a button, type in a box, check a box. The screen has to react. That's state.
1. useState
A variable that changes doesn't update the screen. State does.
π§βπ» Example
Regular variable (does nothing):
function Light() {
let isOn = false;
function toggle() {
isOn = !isOn;
}
return <button onClick={toggle}>{isOn ? "ON" : "OFF"}</button>;
}Click this button and nothing happens on screen. isOn is flipping under the hood, but React never finds out, so it never re-renders.
useState is the fix. Same idea, but now React knows when the value changes:
import { useState } from "react";
function Light() {
const [isOn, setIsOn] = useState(false);
function toggle() {
setIsOn(!isOn);
}
return <button onClick={toggle}>{isOn ? "ON" : "OFF"}</button>;
}useState(false) returns two things: the current value, and a function to update it. Calling the setter does two things: it saves the new value, and it triggers a re-render, React running the component again and updating the screen to match.
useState is called a hook. That's why it starts with use. More on that later.
Rules:
- const [value, setValue] = useState(initialValue)
- Never change state directly (isOn = true is wrong). Always use the setter (setIsOn(true))
- Calling the setter triggers a re-render
π Summary
| Task | Code |
|---|---|
| Create state | const [isOn, setIsOn] = useState(false) |
| Read state | isOn |
| Update state | setIsOn(true) |
| Toggle state | setIsOn(!isOn) |
2. Controlled Inputs
A plain HTML input keeps its own value. React can't see what's typed.
π§βπ» Example
Plain input:
function NameForm() {
return (
<div>
<input type="text" />
<p>Hello, ???</p>
</div>
);
}Type into this box and the browser shows the letters fine, but there's no state holding that value. Nothing else in the app can read it, display it, or react to it.
Same input, tied to state:
import { useState } from "react";
function NameForm() {
const [name, setName] = useState("");
function handleChange(e) {
setName(e.target.value);
}
return (
<div>
<input value={name} onChange={handleChange} />
<p>Hello, {name}</p>
</div>
);
}value={name} sets what's shown, from state. onChange fires on every keystroke and calls handleChange, which updates that state with what was typed. This is called a controlled input: React controls the value, not the DOM.
Rules:
- value comes from state, not from the input itself
- onChange calls a function that updates that state on every keystroke
- e.target.value is the current text in the box
π Summary
| Task | Code |
|---|---|
| Create input state | const [name, setName] = useState("") |
| Control the input | value={name} |
| Update on typing | function handleChange(e) { setName(e.target.value); } |
| Get typed text | e.target.value |
3. Arrays in State
Real apps store lists: guests, messages, products. Add to that list without breaking React.
π§βπ» Example
import { useState } from "react";
function GuestList() {
const [guests, setGuests] = useState([]);
const [name, setName] = useState("");
function handleChange(e) {
setName(e.target.value);
}
function addGuest() {
setGuests([...guests, name]);
setName("");
}
return (
<div>
<input value={name} onChange={handleChange} />
<button onClick={addGuest}>Add</button>
<ul>
{guests.map((guest, i) => (
<li key={i}>{guest}</li>
))}
</ul>
</div>
);
}Type a name, click Add, and it lands in the list. [...guests, name] builds a brand new array with everything from the old one plus the new name. The old array is never touched, that's exactly what React needs to notice the change.
Rules:
- Never push() directly on state. It mutates
- Add: setGuests([...guests, newItem])
- Spread (...) copies the old array into a new one
π Summary
| Task | Code |
|---|---|
| Create array state | const [guests, setGuests] = useState([]) |
| Add an item | setGuests([...guests, name]) |
| Render the list | guests.map((g, i) => <li key={i}>{g}</li>) |
| Never do this | guests.push(name) |
4. Arrays of Objects
State can hold a list of objects, not just plain values. Add a new one the same way: build a new array, never touch the old one.
π§βπ» Example
import { useState } from "react";
function Students() {
const [students, setStudents] = useState([]);
const [name, setName] = useState("");
const [score, setScore] = useState("");
function handleNameChange(e) {
setName(e.target.value);
}
function handleScoreChange(e) {
setScore(e.target.value);
}
function register() {
setStudents([...students, { name, score }]);
setName("");
setScore("");
}
return (
<div>
<input value={name} onChange={handleNameChange} placeholder="Name" />
<input value={score} onChange={handleScoreChange} placeholder="Score" />
<button onClick={register}>Register</button>
{students.map((s, i) => (
<p key={i}>{s.name}: {s.score}</p>
))}
</div>
);
}Type a name and score, click Register, and a new student shows up in the list. { name, score } builds an object from the two inputs. [...students, { name, score }] builds a new array with that object added, same spread pattern as before, just holding objects instead of plain strings.
Rules:
- Build the object first: { name, score }
- Add it the same way as any array: setStudents([...students, newStudent])
- Never push() directly on state
π Summary
| Task | Code |
|---|---|
| Create array of objects | const [students, setStudents] = useState([]) |
| Add a new object | setStudents([...students, { name, score }]) |
| Render the list | students.map((s, i) => <p key={i}>{s.name}: {s.score}</p>) |
| Never do this | students.push({ name, score }) |