From 37b2f73452814f179d853d306c0bbcc3b3b7af42 Mon Sep 17 00:00:00 2001 From: viva-jake Date: Mon, 20 Jan 2025 18:02:34 +0900 Subject: [PATCH 1/3] update useSearchParams to handle function input correctly by passing URLSearchParams instance --- packages/react-router/lib/dom/lib.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 821b6a6457..53c434152e 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1378,7 +1378,9 @@ export function useSearchParams( let setSearchParams = React.useCallback( (nextInit, navigateOptions) => { const newSearchParams = createSearchParams( - typeof nextInit === "function" ? nextInit(searchParams) : nextInit + typeof nextInit === "function" + ? nextInit(new URLSearchParams(searchParams)) + : nextInit ); hasSetSearchParamsRef.current = true; navigate("?" + newSearchParams, navigateOptions); From 518c2dfd12e0bc7bfbacab16f1c2c5375a674bb7 Mon Sep 17 00:00:00 2001 From: viva-jake Date: Mon, 20 Jan 2025 18:23:47 +0900 Subject: [PATCH 2/3] add name to contributors.yml --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index a7a7476510..ff908f6635 100644 --- a/contributors.yml +++ b/contributors.yml @@ -334,6 +334,7 @@ - yionr - yracnet - ytori +- yuhwan-park - yuleicul - zeromask1337 - zheng-chuang From 6ae48899eff52f0b425701b1d88ad9d1f77a720e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 30 Jun 2025 15:54:30 -0400 Subject: [PATCH 3/3] Add changeset and test --- .changeset/curly-sloths-end.md | 5 + .../__tests__/dom/search-params-test.tsx | 114 +++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .changeset/curly-sloths-end.md diff --git a/.changeset/curly-sloths-end.md b/.changeset/curly-sloths-end.md new file mode 100644 index 0000000000..b89d7ea688 --- /dev/null +++ b/.changeset/curly-sloths-end.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Pass a copy of `searchParams` to the `setSearchParams` callback function to avoid muations of the internal `searchParams` instance. This was an issue when navigations were blocked because the internal instance be out of sync with `useLocation().search`. diff --git a/packages/react-router/__tests__/dom/search-params-test.tsx b/packages/react-router/__tests__/dom/search-params-test.tsx index 7afece9c34..e8ddbc586f 100644 --- a/packages/react-router/__tests__/dom/search-params-test.tsx +++ b/packages/react-router/__tests__/dom/search-params-test.tsx @@ -1,7 +1,16 @@ import * as React from "react"; import * as ReactDOM from "react-dom/client"; import { act } from "react-dom/test-utils"; -import { MemoryRouter, Routes, Route, useSearchParams } from "../../index"; +import { + MemoryRouter, + Routes, + Route, + useSearchParams, + createBrowserRouter, + useBlocker, + RouterProvider, + useLocation, +} from "../../index"; describe("useSearchParams", () => { let node: HTMLDivElement; @@ -182,4 +191,107 @@ describe("useSearchParams", () => { `"

value=initial&a=1&b=2

"` ); }); + + it("does not reflect functional update mutation when navigation is blocked", () => { + let router = createBrowserRouter([ + { + path: "/", + Component() { + let location = useLocation(); + let [searchParams, setSearchParams] = useSearchParams(); + let [shouldBlock, setShouldBlock] = React.useState(false); + let b = useBlocker(shouldBlock); + return ( + <> +
+                {`location.search=${location.search}`}
+                {`searchParams=${searchParams.toString()}`}
+                {`blocked=${b.state}`}
+              
+ + + + + ); + }, + }, + ]); + + act(() => { + ReactDOM.createRoot(node).render(); + }); + + expect(node.querySelector("#output")).toMatchInlineSnapshot(` +
+        location.search=
+        searchParams=
+        blocked=unblocked
+      
+ `); + + act(() => { + node + .querySelector("#navigate1")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.querySelector("#output")).toMatchInlineSnapshot(` +
+        location.search=?foo=bar
+        searchParams=foo=bar
+        blocked=unblocked
+      
+ `); + + act(() => { + node + .querySelector("#toggle-blocking")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + act(() => { + node + .querySelector("#navigate2")! + .dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.querySelector("#output")).toMatchInlineSnapshot(` +
+        location.search=?foo=bar
+        searchParams=foo=bar
+        blocked=blocked
+      
+ `); + }); });