Day 4: State and Events
What You'll Learn Today
- Event handling basics
- Using the useState hook
- How state updates work
- Managing multiple states
- Updating state based on previous values
Event Handling Basics
In React, you use event handlers to respond to user actions.
Click Events
function Button() {
function handleClick() {
alert('Clicked!');
}
return <button onClick={handleClick}>Click me</button>;
}
TypeScript version
function Button() {
function handleClick(): void {
alert('Clicked!');
}
return <button onClick={handleClick}>Click me</button>;
}
Event Handler Naming Conventions
| Pattern | Example |
|---|---|
handle + event name |
handleClick, handleSubmit |
on + action |
onSave, onDelete |
function Form() {
function handleSubmit(event) {
event.preventDefault(); // Prevent default behavior
console.log('Form submitted');
}
function handleChange(event) {
console.log('Input value:', event.target.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
TypeScript version
function Form() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>): void {
event.preventDefault(); // Prevent default behavior
console.log('Form submitted');
}
function handleChange(event: React.ChangeEvent<HTMLInputElement>): void {
console.log('Input value:', event.target.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
Inline Event Handlers
For simple operations, you can write handlers inline.
// Inline
<button onClick={() => alert('Click!')}>Click</button>
// Passing arguments
<button onClick={() => handleDelete(id)}>Delete</button>
flowchart LR
subgraph Event["Event Flow"]
A["User action<br/>(click, etc.)"]
B["Event fires"]
C["Handler executes"]
D["UI updates"]
end
A --> B --> C --> D
style A fill:#f59e0b,color:#fff
style D fill:#22c55e,color:#fff
What Is State?
State is data that a component can "remember." When state changes, React automatically re-renders the UI.
Difference from Props
flowchart TB
subgraph Comparison["Props vs State"]
Props["Props<br/>Passed from parent<br/>Read-only"]
State["State<br/>Managed internally<br/>Mutable"]
end
style Props fill:#3b82f6,color:#fff
style State fill:#22c55e,color:#fff
| Feature | Props | State |
|---|---|---|
| Data source | Parent component | Component itself |
| Mutability | Read-only | Mutable |
| Purpose | Configuration, data passing | Dynamic data management |
The useState Hook
useState is a hook that adds state to function components.
Basic Usage
import { useState } from 'react';
function Counter() {
// [current value, update function] = useState(initial value)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
TypeScript version
import { useState } from 'react';
function Counter() {
// [current value, update function] = useState(initial value)
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
useState Structure
const [state, setState] = useState(initialValue);
| Element | Description |
|---|---|
state |
Current state value |
setState |
Function to update state |
initialValue |
Initial state value |
flowchart TB
subgraph useState["How useState Works"]
A["Call useState(0)"]
B["Get [count, setCount]"]
C["Display count"]
D["Call setCount(1)"]
E["Component re-renders"]
F["Display new count (1)"]
end
A --> B --> C
C -->|"Button click"| D
D --> E --> F
style A fill:#3b82f6,color:#fff
style E fill:#f59e0b,color:#fff
Various State Types
function Examples() {
// Number
const [count, setCount] = useState(0);
// String
const [name, setName] = useState('');
// Boolean
const [isVisible, setIsVisible] = useState(false);
// Array
const [items, setItems] = useState([]);
// Object
const [user, setUser] = useState({ name: '', age: 0 });
return (
// ...
);
}
TypeScript version
interface User {
name: string;
age: number;
}
function Examples() {
// Number
const [count, setCount] = useState<number>(0);
// String
const [name, setName] = useState<string>('');
// Boolean
const [isVisible, setIsVisible] = useState<boolean>(false);
// Array
const [items, setItems] = useState<string[]>([]);
// Object
const [user, setUser] = useState<User>({ name: '', age: 0 });
return (
// ...
);
}
How State Updates Work
State Updates Are Asynchronous
State updates don't happen immediately. React batches multiple updates together.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // Still logs 0!
}
return <button onClick={handleClick}>{count}</button>;
}
Batching
Multiple state updates are combined into a single re-render.
function Example() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(count + 1); // Queue update
setFlag(!flag); // Queue update
// β Only 1 re-render, not 2
}
return (
// ...
);
}
Updating Based on Previous Value
For consecutive updates, you need to use the functional form.
Problematic Code
function Counter() {
const [count, setCount] = useState(0);
function handleTripleClick() {
// β This doesn't work as expected!
setCount(count + 1); // count = 0 β 1
setCount(count + 1); // count = 0 β 1 (still references 0)
setCount(count + 1); // count = 0 β 1 (still references 0)
// Result: 1 (not 3)
}
return <button onClick={handleTripleClick}>{count}</button>;
}
Correct Code (Functional Form)
function Counter() {
const [count, setCount] = useState(0);
function handleTripleClick() {
// β
Use functional form
setCount(prev => prev + 1); // 0 β 1
setCount(prev => prev + 1); // 1 β 2
setCount(prev => prev + 1); // 2 β 3
// Result: 3
}
return <button onClick={handleTripleClick}>{count}</button>;
}
flowchart TB
subgraph FunctionalUpdate["Functional Updates"]
A["setCount(prev => prev + 1)"]
B["prev = 0 β 1"]
C["prev = 1 β 2"]
D["prev = 2 β 3"]
end
A --> B --> C --> D
style A fill:#3b82f6,color:#fff
style D fill:#22c55e,color:#fff
When to Use Each Form
| Situation | Form to Use |
|---|---|
| Simple assignment | setState(newValue) |
| Based on previous value | setState(prev => ...) |
| Toggle operation | setState(prev => !prev) |
Managing Multiple States
Independent States
Unrelated data should be managed in separate states.
function UserForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}
TypeScript version
function UserForm() {
const [name, setName] = useState<string>('');
const [age, setAge] = useState<number>(0);
const [email, setEmail] = useState<string>('');
return (
<form>
<input
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAge(Number(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}
Object State
Related data can be grouped as an object.
function UserForm() {
const [user, setUser] = useState({
name: '',
age: 0,
email: ''
});
function handleChange(field, value) {
setUser(prev => ({
...prev, // Copy existing properties
[field]: value // Update specified field
}));
}
return (
<form>
<input
value={user.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={user.age}
onChange={(e) => handleChange('age', Number(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={user.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="Email"
/>
</form>
);
}
TypeScript version
interface UserState {
name: string;
age: number;
email: string;
}
function UserForm() {
const [user, setUser] = useState<UserState>({
name: '',
age: 0,
email: ''
});
function handleChange(field: keyof UserState, value: string | number): void {
setUser(prev => ({
...prev, // Copy existing properties
[field]: value // Update specified field
}));
}
return (
<form>
<input
value={user.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleChange('name', e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={user.age}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleChange('age', Number(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={user.email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleChange('email', e.target.value)}
placeholder="Email"
/>
</form>
);
}
Array State
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// Add
function addTodo() {
if (input.trim()) {
setTodos(prev => [...prev, { id: Date.now(), text: input }]);
setInput('');
}
}
// Delete
function removeTodo(id) {
setTodos(prev => prev.filter(todo => todo.id !== id));
}
// Update
function updateTodo(id, newText) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="New task"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
TypeScript version
interface Todo {
id: number;
text: string;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState<string>('');
// Add
function addTodo(): void {
if (input.trim()) {
setTodos(prev => [...prev, { id: Date.now(), text: input }]);
setInput('');
}
}
// Delete
function removeTodo(id: number): void {
setTodos(prev => prev.filter(todo => todo.id !== id));
}
// Update
function updateTodo(id: number, newText: string): void {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}
return (
<div>
<input
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
placeholder="New task"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Lifting State Up
When multiple components need to share state, move it to their common parent component.
flowchart TB
subgraph Before["Before Lifting"]
A1["ComponentA<br/>state: value"]
B1["ComponentB<br/>state: value"]
end
subgraph After["After Lifting"]
P["Parent<br/>state: value"]
A2["ComponentA<br/>props: value, onChange"]
B2["ComponentB<br/>props: value, onChange"]
end
P --> A2
P --> B2
style Before fill:#ef4444,color:#fff
style After fill:#22c55e,color:#fff
Example: Temperature Converter
// Parent component - manages state
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
const fahrenheit = (celsius * 9/5) + 32;
return (
<div>
<TemperatureInput
label="Celsius"
value={celsius}
onChange={setCelsius}
/>
<TemperatureInput
label="Fahrenheit"
value={fahrenheit}
onChange={(f) => setCelsius((f - 32) * 5/9)}
/>
</div>
);
}
// Child component - no state
function TemperatureInput({ label, value, onChange }) {
return (
<label>
{label}:
<input
type="number"
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
</label>
);
}
TypeScript version
// Parent component - manages state
function TemperatureConverter() {
const [celsius, setCelsius] = useState<number>(0);
const fahrenheit: number = (celsius * 9/5) + 32;
return (
<div>
<TemperatureInput
label="Celsius"
value={celsius}
onChange={setCelsius}
/>
<TemperatureInput
label="Fahrenheit"
value={fahrenheit}
onChange={(f: number) => setCelsius((f - 32) * 5/9)}
/>
</div>
);
}
// Child component - no state
interface TemperatureInputProps {
label: string;
value: number;
onChange: (value: number) => void;
}
function TemperatureInput({ label, value, onChange }: TemperatureInputProps) {
return (
<label>
{label}:
<input
type="number"
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(Number(e.target.value))}
/>
</label>
);
}
Derived State
Values that can be calculated from state shouldn't be stored as state.
function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: 'Product A', price: 10, quantity: 2 },
{ id: 2, name: 'Product B', price: 5, quantity: 3 },
]);
// β Bad: Storing derived value as state
// const [total, setTotal] = useState(0);
// β
Good: Calculate from state
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const itemCount = items.reduce(
(sum, item) => sum + item.quantity,
0
);
return (
<div>
<p>Items: {itemCount}</p>
<p>Total: ${total}</p>
</div>
);
}
TypeScript version
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function ShoppingCart() {
const [items, setItems] = useState<CartItem[]>([
{ id: 1, name: 'Product A', price: 10, quantity: 2 },
{ id: 2, name: 'Product B', price: 5, quantity: 3 },
]);
// Calculate from state
const total: number = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const itemCount: number = items.reduce(
(sum, item) => sum + item.quantity,
0
);
return (
<div>
<p>Items: {itemCount}</p>
<p>Total: ${total}</p>
</div>
);
}
Summary
| Concept | Description |
|---|---|
| Event handlers | Respond to user actions with onClick, etc. |
| useState | Hook to add state to components |
| State updates | Calling setState triggers re-render |
| Functional updates | Use prev => ... for previous value-based updates |
| Lifting state up | Move shared state to parent component |
Key Takeaways
- State updates are asynchronous
- Use functional form for previous value-based updates
- Update objects and arrays with spread syntax to create new references
- Calculate derived values instead of storing as state
- Lift up shared state to parent components
Exercises
Exercise 1: Basics
Create a counter component with three buttons: "+1", "-1", and "Reset" that displays the count value.
Exercise 2: Application
Create an input form that displays "Hello, {name}!" in real-time as you type. Show "Please enter your name" when empty.
Challenge
Create a simple Todo list:
- Text input to add new todos
- Delete button for each todo
- Toggle complete/incomplete
- Display count of incomplete todos
References
- React Docs - State: A Component's Memory
- React Docs - Updating State
- React Docs - Sharing State Between Components
Coming Up Next: On Day 5, we'll learn about "Working with Forms." Understand the difference between controlled and uncontrolled components.