Props let you pass data into a component, but what about data that changes over time? That's where state comes in. State is data that a component owns and can update, triggering a re-render.
useState
The useState hook gives a component its own piece of state:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={()=> setCount(count + 1)}>Increment</button>
</div>
);
}useState(0) returns a pair: the current value (count) and a function to update it (setCount). When you call setCount, React re-renders the component with the new value.
State Updates Are Asynchronous
React batches state updates for performance. If you need to update state based on the previous value, use the functional form:
function Counter() {
const [count, setCount] = useState(0);
function handleTripleIncrement() {
// Wrong — all three read the same stale 'count'
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// Correct — each update builds on the previous
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={handleTripleIncrement}>+3</button>
</div>
);
}Handling Events
React events use camelCase names and receive synthetic event objects that wrap native browser events:
function LoginForm() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log("Form submitted");
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
console.log("Enter pressed");
}
}
return (
<form onSubmit={handleSubmit}>
<input type="text" onKeyDown={handleKeyDown} />
<button type="submit">Log In</button>
</form>
);
}Common event types: onClick, onChange, onSubmit, onKeyDown, onFocus, onBlur.
Controlled Inputs
A controlled input is one whose value is driven by React state. This gives you full control over the input's behavior:
import { useState } from "react";
function SearchForm() {
const [query, setQuery] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
console.log("Searching:", query);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e)=> setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit" disabled={query.length= 0}>
Search
</button>
</form>
);
}The input's value always reflects query, and every keystroke updates the state through onChange. This lets you validate, transform, or filter input in real time.
Managing Object and Array State
When state is an object or array, always create a new reference when updating — never mutate directly:
import { useState } from "react";
interface Todo {
id: number;
text: string;
done: boolean;
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
function addTodo() {
if (!input.trim()) return;
setTodos((prev) => [
...prev,
{ id: Date.now(), text: input.trim(), done: false },
]);
setInput("");
}
function toggleTodo(id: number) {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}
function deleteTodo(id: number) {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}
return (
<div>
<div className="flex gap-2">
<input
value={input}
onChange={(e)=> setInput(e.target.value)}
onKeyDown={(e)=> e.key= "Enter" && addTodo()}
placeholder="Add a task..."
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={todo.done}
onChange={()=> toggleTodo(todo.id)}
/>
<span className={todo.done ? "line-through text-gray-400" : ""}>
{todo.text}
</span>
<button onClick={()=> deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}Key patterns: spread to copy objects ({ ...todo, done: true }), .map() to update one item, .filter() to remove.
Lifting State Up
When two sibling components need to share state, move it to their closest common parent:
import { useState } from "react";
function TemperatureInput({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (val: string) => void;
}) {
return (
<div>
<label className="block text-sm font-medium">{label}</label>
<input
type="number"
value={value}
onChange={(e)=> onChange(e.target.value)}
className="border rounded px-2 py-1"
/>
</div>
);
}
function TemperatureConverter() {
const [celsius, setCelsius] = useState("");
const fahrenheit =
celsius !== "" ? ((parseFloat(celsius) * 9) / 5 + 32).toFixed(1) : "";
return (
<div className="space-y-4">
<TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
<TemperatureInput
label="Fahrenheit"
value={fahrenheit}
onChange={(val)=> {
const c= ((parseFloat(val) - 32) * 5) / 9;
setCelsius(isNaN(c) ? "" : c.toFixed(1));
}}
/>
</div>
);
}The parent (TemperatureConverter) owns the state and passes values and callbacks to both children. This is the standard React pattern for shared state.
Summary
useStategives a component mutable state that triggers re-renders on update.- Use the functional updater (
prev => ...) when the next state depends on the previous. - Use controlled inputs to bind form values to state.
- Never mutate state directly — always create new objects and arrays.
- Lift state up to the nearest common parent when siblings need to share data.