open atlas
↑ Back to track
React patterns, senior RXP · 15 · 03

Capstone III: measure before and after

Close the capstone by measuring the refactor: prove fewer re-renders (Profiler), shallower drilling, the staleness bug gone, and the target change now a single local edit — then stop, because good enough beats perfect.

RXP Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

You inventoried the Dashboard in part 1 and executed the fixes in part 2: the god component split into Toolbar, DataTable, and Detail; the effect-derived filtered state became a value derived during render; the fat context split; the index keys became key={row.id}. The diff looks great. But “looks great” is exactly the claim a senior refactor is not allowed to make.

A refactor that can’t be measured can’t be defended. The whole point of this capstone was to close named line items from the inventory — so now you prove it. Fewer re-renders, on the Profiler. Shallower prop-drilling. The staleness bug provably gone. The target change — “add a column”, “add a filter” — now a single local edit. And then the senior move that beginners skip: you stop, because past the point of payoff, more refactoring is just risk with no return.

Goal

After this lesson you can prove a React refactor improved the two things that matter — render behaviour (measured with React Profiler: fewer commits, narrower commit, lower render count) and changeability (drilling depth, change-locality of the target edit, the derived-state bug gone) — tie each result back to UI = f(state) and the cost-of-change lens, and recognise the stopping point where good enough beats perfect, so you neither claim victory without measuring nor refactor past the point of payoff.

1

Measure render behaviour with the React Profiler, not by eye — count commits and what re-rendered in each. Before/after, record the same interaction (type one character into the filter) under the Profiler. The numbers that matter: how many components committed, and which. Before, a keystroke re-rendered the whole tree because state lived too high and one fat context fed everyone. After, deriving filtered during render and splitting context means a keystroke commits only the Toolbar input and the DataTableDetail and theme consumers don’t.

// before — Profiler on one keystroke: ~14 components commit (whole feature)
// after  — Profiler on one keystroke: 2 commit (Toolbar input + DataTable)
//
// the cause, stated in UI = f(state) terms:
// 'filtered' is now f(data, query) computed in render, and query lives
// next to the input — so changing query only re-runs f for the parts that read it
const filtered = data.filter((r) => r.name.includes(query)); // no effect, no second state

This is the headline metric. “Feels faster” is not a result; “14 commits → 2 commits on a keystroke, captured in the Profiler” is.

2

Measure changeability as drilling depth and change-locality — refactoring is supposed to make the next change cheap. Two countable signals. First, prop-drilling depth: the onSelect callback that threaded Dashboard → Table → TableBody → Row → RowActions (four hops) now reaches its consumer directly via context or lifted markup — depth 4 → 0. Second, and the real prize, change-locality: the target change the feature was built to absorb now touches one place.

// "add a 'status' column" — BEFORE: edit the type, the <th> row, the <td>
// map, the sort switch, AND the fetch shape, scattered across the god component.
//
// AFTER: columns are data, rendering is f(columns), so it's one local edit:
const columns: Column<Row>[] = [
  { key: "name", header: "Name" },
  { key: "email", header: "Email" },
  { key: "status", header: "Status" }, // <- the whole change
];
// <DataTable columns={columns} rows={filtered} /> renders f(columns, rows)

A change that was five scattered edits is now one. That delta — not the line count of the diff — is what you report.

3

Prove the correctness win the same way you proved the perf win: show the derived-state bug is structurally gone, not just unobserved. The P0 smell from part 1 was a staleness bug — filtered copied from data + query by an effect, stale by a frame and fragile to a missed dependency. “I didn’t see it anymore” is not proof; the effect could still misfire. The proof is structural: there is no second state and no effect, so there is no window in which filtered can disagree with data and query.

// before — two sources of truth, reconciled late by an effect (stale-by-a-frame)
const [filtered, setFiltered] = useState<Row[]>([]);
useEffect(() => { setFiltered(data.filter((r) => r.name.includes(query))); }, [data, query]);

// after — one source of truth; the bug class cannot exist because there is no copy
const filtered = data.filter((r) => r.name.includes(query));

This is UI = f(state) enforced: derive, don’t duplicate. The win is reported as “bug class eliminated” — the staleness can’t recur because the state it lived in is gone.

4

Know when to STOP: a refactor has a point of payoff, and going past it is risk without return. You closed every inventory line item; the Profiler is green; the target change is local; the bug is gone. The junior temptation now is to keep going — memoize every component, extract three more hooks, make DataTable infinitely generic for columns that don’t exist yet. That is not seniority, it’s the over-engineering this whole track warns against, now wearing a refactor’s clothes. Each extra abstraction adds indirection and a chance to introduce a regression, for a speculative future change that may never arrive.

// STOP signal: the next "improvement" optimizes a change nobody has asked for.
// - memo() on a component the Profiler shows commits twice a session → no payoff
// - a generic <DataTable> plugin system for one table → YAGNI, pure cost
// good enough (measured, local, correct) beats perfect (speculative, risky)

The stopping rule is the cost-of-change lens applied to the refactor itself: stop when the next edit’s cost no longer exceeds the cost (and risk) of the abstraction that would reduce it. Past that line, you are spending to make an imaginary change cheaper.

Worked example

The before/after measurement table — the deliverable that closes the capstone. You don’t ship “I refactored the Dashboard.” You ship this, each row tracing to an inventory line item from part 1.

// REFACTOR RESULT — Dashboard feature  (each row closes a P0/P1/P2 from part 1)
//
// metric                        before          after        how measured
// ----------------------------------------------------------------------------
// commits on one keystroke      ~14 (whole)     2            React Profiler, same interaction
// components re-rendered        Toolbar+Table   Toolbar+      Profiler "what rendered"
//   on filter change            +Detail+theme   DataTable
// prop-drilling depth (onSelect) 4 hops         0 hops       count pass-through components
// "add a column" change         5 scattered     1 local      edit count to add 'status'
//   locality                    edits           edit
// derived-state staleness (P0)  present         eliminated   no second state / no effect exists
// index-key corruption (P0)     present         fixed        key={row.id}; reorder keeps row state

Now read it through the two lenses the track is built on. UI = f(state): the render win exists because filtered is computed from state during render and state was relocated next to its reader, so f re-runs only where its inputs changed — that is the commit-count drop. Cost-of-change: the “add a column” row going from 5 edits to 1 is the entire justification for the refactor; a faster render that didn’t make change cheaper would be a weak result, and a cheaper change with no perf win is still a real win.

Then the stopping check. Every P0/P1/P2 line item is closed. The next candidate — wrapping DataTable in memo — the Profiler shows commits twice per session, so it saves nothing measurable. You stop here. Writing that table, tying it to the two lenses, and choosing to stop is the senior deliverable. The diff was the easy part.

Why this works

Why measure at all, when the after-code is obviously better? Because “obviously better” is how refactors quietly fail review and quietly fail to pay off. A reviewer can’t approve a perf claim they can’t see; a future-you can’t trust that the staleness bug is gone if no one wrote down why it can’t recur. Measuring converts a refactor from an aesthetic argument (“cleaner”) into an engineering one (“14 → 2 commits, add-a-column went 5 edits → 1, staleness bug class eliminated”). The first kind gets bikeshedded; the second kind gets merged. Measurement is what makes the work defensible to someone who didn’t watch you do it.

Common mistake

Two symmetric failure modes bracket this lesson. The first is claiming success without measuring: shipping “refactored Dashboard, much cleaner” with no Profiler capture and no change-locality count — you might have moved lines without reducing renders, or even added commits. The second is refactoring past the point of payoff: the Profiler is already green and the change is already local, but you keep memoizing, extracting, and generalizing for changes nobody asked for, adding indirection and regression risk for zero measured gain. Senior judgment lives exactly between them: prove the win with numbers, and stop the moment the next edit’s payoff no longer exceeds the abstraction’s cost. Good enough, measured, beats perfect, imagined.

Check yourself
Quiz

You finished the Dashboard refactor. The Profiler shows a keystroke went from ~14 commits to 2, and 'add a column' went from 5 scattered edits to 1. A teammate suggests wrapping every leaf component in memo() 'to be thorough' before calling it done. What's the senior call?

Recap

A refactor closes with proof, not vibes. Measure the two things that matter: render behaviour with the React Profiler (commit count and what re-rendered — ~14 → 2 on a keystroke is a result; “feels faster” is not) and changeability (prop-drilling depth 4 → 0, and the target change going from scattered edits to one local edit), plus the correctness win shown structurally — the derived-state staleness bug is gone as a class because the duplicated state and its effect no longer exist. Tie every number back to the track’s two lenses: the render drop is UI = f(state) re-running only where its inputs moved, and the change-locality drop is the cost-of-change lens — a refactor that didn’t make the next change cheaper barely refactored. Ship the before/after table where each row closes a named inventory item. Then stop: when every line item is closed and the next “improvement” optimizes a change nobody asked for, more refactoring is risk without return. The senior bar is to prove improvement in render and changeability, and to know that good enough, measured, beats perfect, imagined. That judgment — match the work to the payoff, and quit at the line — is where this track has been pointing all along.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.