Good Code
The good version asks React for the latest selected IDs before calculating the next state.
Lesson 04
Use functional state updates when the next value depends on the current value.
import { useState } from "react";
type Reviewer = {
id: string;
name: string;
};
export function ReviewerPicker({ reviewers }: { reviewers: Reviewer[] }) {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
function toggleReviewer(id: string) {
// Functional updates read the latest state React has queued.
setSelectedIds((currentIds) =>
currentIds.includes(id)
? currentIds.filter((currentId) => currentId !== id)
: [...currentIds, id],
);
}
return reviewers.map((reviewer) => (
<button
key={reviewer.id}
type="button"
aria-pressed={selectedIds.includes(reviewer.id)}
onClick={() => toggleReviewer(reviewer.id)}
>
{reviewer.name}
</button>
));
}import { useState } from "react";
type Reviewer = {
id: string;
name: string;
};
export function ReviewerPicker({ reviewers }: { reviewers: Reviewer[] }) {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
function toggleReviewer(id: string) {
// This uses the selectedIds captured by the current render.
if (selectedIds.includes(id)) {
setSelectedIds(selectedIds.filter((currentId) => currentId !== id));
return;
}
setSelectedIds([...selectedIds, id]);
}
return reviewers.map((reviewer) => (
<button
key={reviewer.id}
type="button"
aria-pressed={selectedIds.includes(reviewer.id)}
onClick={() => toggleReviewer(reviewer.id)}
>
{reviewer.name}
</button>
));
}The good version asks React for the latest selected IDs before calculating the next state.
The bad version calculates from the value captured by the current render. That can lose updates when multiple events or batched changes happen close together.