fix(react-router): treat Link params and search as immutable inputs - #8380
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughThe change updates Link destination stability to preserve references for deeply equal values. Tests now verify that equal inline literals reuse locations and nested value changes rebuild them. ChangesLink destination stability
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to Link destination reuse now consistently treats destination inputs as immutable, with coverage for equal literals and nested state-driven updates. No merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit 98a9b40
☁️ Nx Cloud last updated this comment at |
🚀 Changeset Version Preview7 package(s) bumped directly, 22 bumped as dependents. 🟩 Patch bumps
|
Bundle Size Benchmarks
The following scenarios have bundle-size changes compared with the baseline:
Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better. |
`useValueStable` kept a shallow copy of `params` / `search` / `activeOptions` (dac2bf2) so that an object mutated in place would yield a new reference on the next render. That only ever held one level deep: a nested change such as `search.filters.page = 2` left the shared nested reference in the copy, `deepEqual` short-circuited on it, `dest` kept its identity and `buildLocation` served the cached location. Detecting mutation is not a contract React lets us keep. A mutation on its own never renders anything; the tests that exercised it needed a manual re-render button to observe the change, which is the same situation as a mutated `React.memo` prop or `useMemo` dependency. Rather than snapshot deeper (measured +63 gzip bytes on react-router.minimal and a 3-5x slower compare whenever nested references are shared, since nothing can short-circuit), `useValueStable` stores the caller's reference again, as on main. The reference is the router's invalidation signal: pass a new object to change a destination. The regression test now pins the contract that can actually break: fresh inline literals with equal nested contents must not trigger another `buildLocation`, and a nested value that changes through React state must produce a new href on that render and keep it across navigations. The accessor and in-place mutation tests are removed along with the promise in the changeset. Bundle (react-router.minimal gzip): 86038 -> 86026 (-12). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
6163e6f to
98a9b40
Compare
…8380) `useValueStable` kept a shallow copy of `params` / `search` / `activeOptions` (dac2bf2) so that an object mutated in place would yield a new reference on the next render. That only ever held one level deep: a nested change such as `search.filters.page = 2` left the shared nested reference in the copy, `deepEqual` short-circuited on it, `dest` kept its identity and `buildLocation` served the cached location. Detecting mutation is not a contract React lets us keep. A mutation on its own never renders anything; the tests that exercised it needed a manual re-render button to observe the change, which is the same situation as a mutated `React.memo` prop or `useMemo` dependency. Rather than snapshot deeper (measured +63 gzip bytes on react-router.minimal and a 3-5x slower compare whenever nested references are shared, since nothing can short-circuit), `useValueStable` stores the caller's reference again, as on main. The reference is the router's invalidation signal: pass a new object to change a destination. The regression test now pins the contract that can actually break: fresh inline literals with equal nested contents must not trigger another `buildLocation`, and a nested value that changes through React state must produce a new href on that render and keep it across navigations. The accessor and in-place mutation tests are removed along with the promise in the changeset. Bundle (react-router.minimal gzip): 86038 -> 86026 (-12). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Stacked on #8370.
useValueStablekept a shallow copy ofparams/search/activeOptionsso that an object mutated in place would yield a new reference on the next render. As CodeRabbit noted on #8370, that only ever held one level deep:search.filters.page = 2leaves the shared nested reference in the copy,deepEqualshort-circuits on it,destkeeps its identity andbuildLocationserves the cached location.Detecting mutation is not a contract React lets us keep — a mutation on its own never renders anything; the tests exercising it needed a manual re-render button. Rather than snapshot deeper (measured +63 gzip on
react-router.minimal, and a 3–5× slower per-render compare whenever nested references are shared, since nothing can short-circuit),useValueStablestores the caller's reference again, exactly as onmain. The reference is the router's invalidation signal: pass a new object to change a destination, like any other React prop.Changes
link.tsx:useValueStablestores the reference; comment states the contract.link-destination.test.tsx: removes the accessor / in-place mutation tests and theirRerenderfixture hack. Adds one regression test for what can actually break: fresh inline literals with equal nested contents must not trigger anotherbuildLocation(spy), and a nested value changed through React state must produce a new href on that render and keep it across navigations. Verified the test fails whenuseValueStableis neutered..changeset/still-links-rest.md: replaces the "mutated in place is picked up on the next render" promise with the immutable-inputs contract.Measurements
useValueStablevariantreact-router.minimalgzipmain)Solid and Vue are unaffected: their Links build a fresh options object per read and never hit the
buildLocationcache.Verification
@tanstack/react-routertest:unit(Link suites, 181 tests),test:types,test:eslintpnpm benchmark:bundle-size:run --scenario react-router.minimal: 86038 → 86026Summary by CodeRabbit
Bug Fixes
params,search, andactiveOptionscontain unchanged values.Documentation