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/contributors.yml b/contributors.yml index 805d06cd79..2935d4d650 100644 --- a/contributors.yml +++ b/contributors.yml @@ -403,6 +403,7 @@ - yionr - yracnet - ytori +- yuhwan-park - yuleicul - zeevick10 - zeromask1337 diff --git a/packages/react-router/__tests__/dom/search-params-test.tsx b/packages/react-router/__tests__/dom/search-params-test.tsx index 7860d1a320..38002b2065 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 "@testing-library/react"; -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
+      
+ `); + }); }); diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 2d6522c26c..df6c0fa2de 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -1424,7 +1424,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);