1 / 3
Bundle the updates that have to happen together
Nobody got it wrong. Everyone carefully copied the code closest to hand.
A year ago
An admin screen that listed products. At first it had only two pieces of state: the search query and the page.
When the query changed, the page went back to 1. Two lines were enough.
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
function onSearch(q: string) {
setQuery(q);
setPage(1);
}Over the next year
Requests came in one at a time. Category, sort order, price range, in-stock only, and a bulk selection for unpublishing several products at once.
A different person each time, and each time it was due Friday. The state grew to eight variables, and the code that changed them spread across four files.
The rules grew too. Changing a filter changes the results, so reset to page 1 and clear the selection. Changing the sort keeps the same results, so reset only the page and keep the selection. That difference lived only in the heads of whoever wrote each file.
const [query, setQuery] = useState("");
const [category, setCategory] = useState("all");
const [sort, setSort] = useState<Sort>("newest");
const [minPrice, setMinPrice] = useState<number | null>(null);
const [maxPrice, setMaxPrice] = useState<number | null>(null);
const [inStock, setInStock] = useState(false);
const [page, setPage] = useState(1);
const [selected, setSelected] = useState<string[]>([]);// Filter: page 1 + clear selection
<CategorySelect
onChange={(c) => {
setCategory(c);
setPage(1);
setSelected([]);
}}
/>// Sort: page 1, keep selection
<Dropdown
onChange={(v) => {
setSort(v);
setPage(1);
}}
/>// Restoring from the URL re-implements the same rule
useEffect(() => {
const q = parse(location.search);
if (q.category !== category) {
setCategory(q.category);
setPage(1);
setSelected([]);
}
// ...
}, [location.search]);The brand filter
The new developer was diligent. Instead of starting from scratch, they looked for existing code. A component that picks one value from a dropdown: the closest match was SortSelect.tsx. They copied it.
Nothing anywhere said that sorting and filtering follow different rules. The reviewer approved it with "Nice, follows the existing pattern."
// Copied from SortSelect.tsx
<Dropdown
onChange={(b) => {
setBrand(b);
setPage(1);
}}
/>
// The selection is not clearedMonday morning
Someone on the operations team selected 30 products from brand A, whose sale had just ended. Then, to check something, they switched the brand filter to B. They picked 5 brand B products and clicked "Unpublish selected."
35 products were unpublished. That included the 30 brand A products that were no longer on screen. Some of them had ads running that morning.
Why it happened
Nobody wrote wrong code. Everyone followed the nearest code. The problem was that there were two kinds of code to follow, and nothing in the eight pieces of state or the four files said which one applied.
With two pieces of state, the rules are visible. With eight, the rules live in people's memory instead of in the code. And people move on.
Before it breaks
Put "changes that alter the results" and "changes that keep the results" into one function each. Whether the page resets or the selection clears is decided by that function, not by whoever calls it.
Now anyone adding a brand filter only has to call changeFilter. The page reset and the cleared selection come with it. Restoring from the URL calls the same function. There is no wrong code left to copy.
A reducer, a store, a plain function: the shape doesn't matter, and neither does the framework. What matters is bundling the updates that have to happen together into one unit, so nobody can call them separately.
export type ListState = {
filters: Filters;
sort: Sort;
page: number;
selected: string[];
};
// Results change → reset page and selection
export function changeFilter(s: ListState, patch: Partial<Filters>): ListState {
return { ...s, filters: { ...s.filters, ...patch }, page: 1, selected: [] };
}
// Results stay the same → keep the selection
export function changeSort(s: ListState, sort: Sort): ListState {
return { ...s, sort, page: 1 };
}<Dropdown
onChange={(brand) => setList((s) => changeFilter(s, { brand }))}
/>useEffect(() => {
setList((s) => changeFilter(s, parse(location.search)));
}, [location.search]);