State is the memory of your components. In this part, we’ll:
- Learn when to use useState vs useReducer.
- Build typed reducers.
- Create custom hooks for reusable state logic.
🎯 1. Recap: useState
You already know useState holds a single value:
import { useState } from 'react';
export function Counter() {
// count: number, setCount: function to update it
const [count, setCount] = useState(0); // inferred as number
return (
<div>
<p>Count: {count}</p>
{/* onClick updates state */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
👉 Perfect for simple local state.
🎯 2. When to use useReducer
If state has multiple fields or complex updates, useReducer is better.
Example: a Task object.
// Define a Task type
type Task = { id: string; title: string; done: boolean };
// State is an array of tasks
type State = Task[];
// Define all possible actions
type Action =
| { type: 'add'; title: string }
| { type: 'toggle'; id: string }
| { type: 'remove'; id: string };
// Reducer function: receives current state + action → returns new state
function tasksReducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
// create new task with random id
return [...state, { id: crypto.randomUUID(), title: action.title, done: false }];
case 'toggle':
// flip the "done" flag
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'remove':
// remove task by id
return state.filter(t => t.id !== action.id);
default: {
// exhaustive check: ensures all Action types are handled
const _exhaustive: never = action;
return state;
}
}
}
Usage:
import { useReducer, useState } from 'react';
export function TaskList() {
// tasks: State, dispatch: function to send actions
const [tasks, dispatch] = useReducer(tasksReducer, []);
const [title, setTitle] = useState('');
const addTask = () => {
if (title.trim()) {
dispatch({ type: 'add', title }); // dispatch an "add" action
setTitle('');
}
};
return (
<div>
{/* controlled input */}
<input value={title} onChange={e => setTitle(e.target.value)} />
<button onClick={addTask}>Add</button>
<ul>
{tasks.map(t => (
<li key={t.id}>
<label>
{/* toggle checkbox dispatches a "toggle" action */}
<input
type="checkbox"
checked={t.done}
onChange={() => dispatch({ type: 'toggle', id: t.id })}
/>
{t.title}
</label>
{/* delete button dispatches a "remove" action */}
<button onClick={() => dispatch({ type: 'remove', id: t.id })}>❌</button>
</li>
))}
</ul>
</div>
);
}
👉 With useReducer, your state updates are predictable, and TypeScript ensures actions are always valid.
🎯 3. Extracting Logic with Custom Hooks
If multiple components need the same logic, extract it.
// src/hooks/useTasks.ts
import { useReducer } from 'react';
// Define Task type
type Task = { id: string; title: string; done: boolean };
// State is array of tasks
type State = Task[];
// All actions our reducer can handle
type Action =
| { type: 'add'; title: string }
| { type: 'toggle'; id: string }
| { type: 'remove'; id: string };
// Reducer function
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return [...state, { id: crypto.randomUUID(), title: action.title, done: false }];
case 'toggle':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'remove':
return state.filter(t => t.id !== action.id);
default:
return state; // fallback (shouldn’t happen with exhaustive typing)
}
}
// Custom hook that encapsulates tasks logic
export function useTasks(initial: Task[] = []) {
const [tasks, dispatch] = useReducer(reducer, initial);
// return both tasks state and dispatcher to the caller
return { tasks, dispatch };
}
Usage:
import { useTasks } from '@/hooks/useTasks';
export function TaskApp() {
const { tasks, dispatch } = useTasks();
return (
<ul>
{tasks.map(t => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
👉 Now any component can use useTasks and get a fully typed task manager.
🎯 4. Visual Example for Blog Readers
Show before/after:
- Before → scattered state logic in each component.
- After → clean reducer + hook that centralizes state logic.
✅ Wrap-Up
In Part 4, you learned:
- When to use
useStatevsuseReducer. - How to create a typed reducer with exhaustive checks.
- How to move logic into a custom hook for reusability.
👉 Next: We’ll explore Routing with React Router — setting up pages, navigation, and type-safe route parameters.
Comments
Post a Comment