From d0354968944cb62e093318d9bea29ef1f53fa1ca Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 15 Feb 2026 01:48:17 -0800 Subject: [PATCH 1/3] fix: make setSearchParams return a stable reference setSearchParams was being recreated on every search param change because searchParams was included in its useCallback dependency array. This caused unnecessary re-renders in child components that received setSearchParams as a prop, even when those children didn't depend on the search params value. The fix uses a ref to track the current searchParams, allowing the callback to always access the latest value without being a dependency. This matches the behavior of React's useState setter which is always stable. Fixes #9991 --- .../__tests__/dom/search-params-test.tsx | 167 ++++++++++++++++++ packages/react-router/lib/dom/lib.tsx | 7 +- 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/dom/search-params-test.tsx b/packages/react-router/__tests__/dom/search-params-test.tsx index 33ed179985..faee0f08c4 100644 --- a/packages/react-router/__tests__/dom/search-params-test.tsx +++ b/packages/react-router/__tests__/dom/search-params-test.tsx @@ -295,3 +295,170 @@ describe("useSearchParams", () => { `); }); }); + +describe("useSearchParams setSearchParams stability", () => { + let node: HTMLDivElement; + beforeEach(() => { + node = document.createElement("div"); + document.body.appendChild(node); + }); + + afterEach(() => { + document.body.removeChild(node); + node = null!; + }); + + it("returns a stable setSearchParams reference across re-renders caused by search param changes", () => { + let setSearchParamsRefs: Array = []; + + function SearchPage() { + let [searchParams, setSearchParams] = useSearchParams(); + setSearchParamsRefs.push(setSearchParams); + + return ( +
+

The current query is "{searchParams.get("q") || ""}".

+ +
+ ); + } + + act(() => { + ReactDOM.createRoot(node).render( + + + } /> + + , + ); + }); + + expect(node.innerHTML).toMatch(/The current query is "initial"/); + expect(setSearchParamsRefs.length).toBe(1); + + // Click the button to update search params + act(() => { + node + .querySelector("button")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.innerHTML).toMatch(/The current query is "new-value"/); + expect(setSearchParamsRefs.length).toBe(2); + + // The setSearchParams reference should be the same across renders + expect(setSearchParamsRefs[0]).toBe(setSearchParamsRefs[1]); + }); + + it("does not cause unnecessary re-renders in child components that depend only on setSearchParams", () => { + let childRenderCount = 0; + + let Child = React.memo(function Child({ + setSearchParams, + }: { + setSearchParams: Function; + }) { + childRenderCount++; + return ( + + ); + }); + + function Parent() { + let [searchParams, setSearchParams] = useSearchParams(); + return ( +
+

Count: {searchParams.get("count") || "0"}

+ +
+ ); + } + + act(() => { + ReactDOM.createRoot(node).render( + + + } /> + + , + ); + }); + + expect(node.innerHTML).toMatch(/Count: 0/); + expect(childRenderCount).toBe(1); + + // Click to increment + act(() => { + node + .querySelector("button")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.innerHTML).toMatch(/Count: 1/); + // Child should not have re-rendered since setSearchParams is stable + expect(childRenderCount).toBe(1); + }); + + it("provides the latest search params value in functional updates even with a stable reference", () => { + function SearchPage() { + let [searchParams, setSearchParams] = useSearchParams(); + + return ( +
+

Count: {searchParams.get("count") || "0"}

+ +
+ ); + } + + act(() => { + ReactDOM.createRoot(node).render( + + + } /> + + , + ); + }); + + expect(node.innerHTML).toMatch(/Count: 0/); + + // Increment multiple times + act(() => { + node + .querySelector("#increment")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.innerHTML).toMatch(/Count: 1/); + + act(() => { + node + .querySelector("#increment")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.innerHTML).toMatch(/Count: 2/); + }); +}); diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 39bc64675f..3440e8188b 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -2309,18 +2309,21 @@ export function useSearchParams( [location.search], ); + let searchParamsRef = React.useRef(searchParams); + searchParamsRef.current = searchParams; + let navigate = useNavigate(); let setSearchParams = React.useCallback( (nextInit, navigateOptions) => { const newSearchParams = createSearchParams( typeof nextInit === "function" - ? nextInit(new URLSearchParams(searchParams)) + ? nextInit(new URLSearchParams(searchParamsRef.current)) : nextInit, ); hasSetSearchParamsRef.current = true; navigate("?" + newSearchParams, navigateOptions); }, - [navigate, searchParams], + [navigate], ); return [searchParams, setSearchParams]; From 3ef3b42f9273fcdec32d6be3daaa5cd1b6e7dbf3 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 15 Feb 2026 02:23:11 -0800 Subject: [PATCH 2/3] add changeset for stable setSearchParams fix Co-Authored-By: Claude Opus 4.6 --- .changeset/stable-set-search-params.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stable-set-search-params.md diff --git a/.changeset/stable-set-search-params.md b/.changeset/stable-set-search-params.md new file mode 100644 index 0000000000..33daec70c6 --- /dev/null +++ b/.changeset/stable-set-search-params.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +fix: make setSearchParams return a stable reference From 1c2445263b35abcd9bc4af81184af0f5483a53fd Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sun, 15 Feb 2026 02:23:37 -0800 Subject: [PATCH 3/3] sign CLA by adding veeceey to contributors.yml Co-Authored-By: Claude Opus 4.6 --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index daa45d0f08..37cc689044 100644 --- a/contributors.yml +++ b/contributors.yml @@ -440,6 +440,7 @@ - valerii15298 - ValiantCat - vdusart +- veeceey - vesan - vezaynk - VictorElHajj