Why adding a server call broke scrolling
I was building a small app and hit a strange bug. A route displayed a sequence of cards, each with a button that scrolled down to the next card. Scrolling worked perfectly. Then I added a server call to that button, and scrolling started jittering and breaking.
The obvious suspect was React Router's action mechanism triggering a data refetch and re-render. But a refetch shouldn't interfere with an ongoing scrollIntoView call. The server returned identical items, so nothing should have changed. In React, a re-render should always be safe. Something else was wrong — in my code, in React Router, in React, or even in the browser itself.
The "just fix it" trap
My first instinct was to hand the problem to Claude. It rewrote the useEffect containing scrollIntoView, said the bug was fixed, and it wasn't. It tried switching smooth scrolling to instant. Same result: proudly declared solved, still broken.
This isn't a complaint about Claude. Human engineers, myself included, make the same mistake. The core issue was that Claude had no repro. My bug report — "click the button, scroll jitters" — is useless to an agent that can't see the screen. Claude was operating without a verifiable test, guessing at fixes instead of confirming them.
Building a repro Claude could use
A repro is a set of instructions that reliably tells you whether the bug is present: what to do, what's expected, and what actually happens. My original repro was reliable (the bug happened every time) but unusable for Claude. The jitter is visual; a few screenshots wouldn't capture it.
When a bug can't be seen or reproduced by someone else, you can trade it for another repro — as long as you can convince yourself the new one makes progress on the original problem. Here's the one I suggested:
- Measure the document scroll position.
- Click the button.
- Measure the document scroll position again.
- Expected: the positions differ. Actual: they're identical.
This doesn't capture the jitter, but if the scroll position never changes, that's closely related and worth fixing on its own. Claude added console.logs, drove the page through Playwright MCP, and confirmed the scroll position wasn't changing after the click. Now Claude could verify the bug.
Validate a new repro against known fixes
There's a subtle danger in narrowing a repro: you might be capturing a different problem that looks similar. For example, Claude might have been reading scroll position too early, in which case even correct fixes would show as failures. So when you change a repro, you must confirm that a positive result is still achievable.
Concretely, I told Claude to comment out the network call — something we already knew fixed the original bug. If the new repro truly captures the same problem, this change should also make the scroll positions differ. It did. We toggled the network call in and out several times, and each time the repro result matched our prediction.
This doesn't guarantee the new repro captures the same problem. But a network call affecting scroll position is just as suspicious as the original jitter, and it's worth solving regardless. That's enough justification to proceed.
Reduce with a safety net
At this point I created a new branch and gave Claude a strict workflow:
- Run the repro, verify the bug is present.
- Remove something from the relevant code — components, handlers, conditions, styles, imports.
- Run the repro again.
- If the bug is still there, commit the change and repeat.
- If the bug is gone, write down a theory about what "solved it," reset to the last commit, and try removing a smaller chunk.
The critical invariant: at every point, you have a checkpoint where the bug still reproduces, and each step reduces the surface area. You must never lose the bug while working toward its cause.
When I came back, Claude presented several repro cases. None of them reproduced the bug. The problem was that Claude had started forming theories — maybe this effect was buggy, maybe remounting, maybe React — and tested those in isolation instead of following the reduction process. When a theory failed, the correct move was to return to the original working repro, not branch off into new experiments.
Claude had lost the recursion: it wasn't proving that with every step the repro got smaller. This is like writing fib(n) that calls itself instead of fib(n - 2) — it never terminates because nothing gets smaller. Lean rejects such ill-founded recursion as a type error, statically proving that the argument decreases. Bug reduction needs the same discipline: each step must preserve the bug while shrinking the repro. If you keep the repro alive and keep removing pieces, eventually you run out of things to remove. That leaves you with either a mistake in your code or a bug in something you can't reduce further — like React itself.
Repeat until you find it.
Narrowing Down the Culprit
Claude didn't end up solving this bug, but it came close. The key was getting it to use fewer, more careful removals until the problem was isolated to one file. Moving that file out of the router made the same code work; placing it back broke things again. Making it a top-level route also worked. Something about being nested inside the root layout triggered the failure.
The root layout configuration looked like this:
import { Outlet, ScrollRestoration } from "react-router-dom";
export function RootLayout() {
return (
<div>
<ScrollRestoration />
<Outlet />
</div>
);
}The culprit turned out to be a known issue in React Router, since fixed in June, where ScrollRestoration ran on every revalidation instead of on route changes. Because the network call ran through an action and triggered a revalidation, ScrollRestoration would fire during scrollIntoView and cause the visible jitter.
This method—systematically removing code while confirming the bug remains—has been remarkably effective across many debugging sessions. There have been cases where chasing a bug meant deleting large portions of an application's component tree, eventually landing on a minimal repro of about 50 lines. Once plausible theories are exhausted, this is often the most reliable path forward.
Notably, this issue might have been avoided entirely with the latest React Router version. The project's dependency was pinned to an older version by Claude, a decision that led to this unnecessary detour. A reminder that setup choices matter, even when using AI-assisted development.



