diff --git a/.changeset/afraid-ducks-know.md b/.changeset/afraid-ducks-know.md new file mode 100644 index 0000000000..6c004a0cb6 --- /dev/null +++ b/.changeset/afraid-ducks-know.md @@ -0,0 +1,6 @@ +--- +"react-router-dom": patch +"react-router-native": patch +--- + +Fix bug with search params removal diff --git a/packages/react-router-dom/__tests__/search-params-test.tsx b/packages/react-router-dom/__tests__/search-params-test.tsx index 9988493cd1..298914fbec 100644 --- a/packages/react-router-dom/__tests__/search-params-test.tsx +++ b/packages/react-router-dom/__tests__/search-params-test.tsx @@ -125,4 +125,40 @@ describe("useSearchParams", () => { ); expect(node.innerHTML).toMatch(/The new query is "Ryan Florence"/); }); + + it("allows removal of search params when a default is provided", () => { + function SearchPage() { + let [searchParams, setSearchParams] = useSearchParams({ + value: "initial", + }); + + return ( +
+

The current value is "{searchParams.get("value")}".

+ +
+ ); + } + + act(() => { + ReactDOM.createRoot(node).render( + + + } /> + + + ); + }); + + let button = node.querySelector("button")!; + expect(button).toBeDefined(); + + expect(node.innerHTML).toMatch(/The current value is "initial"/); + + act(() => { + button.dispatchEvent(new Event("click", { bubbles: true })); + }); + + expect(node.innerHTML).toMatch(/The current value is ""/); + }); }); diff --git a/packages/react-router-dom/dom.ts b/packages/react-router-dom/dom.ts index 21d9b84a15..a87ff65715 100644 --- a/packages/react-router-dom/dom.ts +++ b/packages/react-router-dom/dom.ts @@ -88,15 +88,17 @@ export function createSearchParams( export function getSearchParamsForLocation( locationSearch: string, - defaultSearchParams: URLSearchParams + defaultSearchParams: URLSearchParams | null ) { let searchParams = createSearchParams(locationSearch); - for (let key of defaultSearchParams.keys()) { - if (!searchParams.has(key)) { - defaultSearchParams.getAll(key).forEach((value) => { - searchParams.append(key, value); - }); + if (defaultSearchParams) { + for (let key of defaultSearchParams.keys()) { + if (!searchParams.has(key)) { + defaultSearchParams.getAll(key).forEach((value) => { + searchParams.append(key, value); + }); + } } } diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index ff43c18918..2ff9d90449 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -853,13 +853,17 @@ export function useSearchParams( ); let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); + let hasSetSearchParamsRef = React.useRef(false); let location = useLocation(); let searchParams = React.useMemo( () => + // Only merge in the defaults if we haven't yet called setSearchParams. + // Once we call that we want those to take precedence, otherwise you can't + // remove a param with setSearchParams({}) if it has an initial value getSearchParamsForLocation( location.search, - defaultSearchParamsRef.current + hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current ), [location.search] ); @@ -870,6 +874,7 @@ export function useSearchParams( const newSearchParams = createSearchParams( typeof nextInit === "function" ? nextInit(searchParams) : nextInit ); + hasSetSearchParamsRef.current = true; navigate("?" + newSearchParams, navigateOptions); }, [navigate, searchParams] diff --git a/packages/react-router-native/__tests__/__snapshots__/search-params-test.tsx.snap b/packages/react-router-native/__tests__/__snapshots__/search-params-test.tsx.snap index fb3ab6db85..a436880c7d 100644 --- a/packages/react-router-native/__tests__/__snapshots__/search-params-test.tsx.snap +++ b/packages/react-router-native/__tests__/__snapshots__/search-params-test.tsx.snap @@ -1,5 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`useSearchParams allows removal of search params when a default is provided 1`] = ` + + + The current query is " + initial + ". + + + Click + + +`; + +exports[`useSearchParams allows removal of search params when a default is provided 2`] = ` + + + The current query is " + ". + + + Click + + +`; + exports[`useSearchParams reads and writes the search string (functional update) 1`] = ` diff --git a/packages/react-router-native/__tests__/search-params-test.tsx b/packages/react-router-native/__tests__/search-params-test.tsx index f3265cd2c5..98c851373c 100644 --- a/packages/react-router-native/__tests__/search-params-test.tsx +++ b/packages/react-router-native/__tests__/search-params-test.tsx @@ -18,6 +18,10 @@ describe("useSearchParams", () => { return {children}; } + function Button({ children }: { children: React.ReactNode; onClick?: any }) { + return {children}; + } + it("reads and writes the search string", () => { function SearchPage() { let [searchParams, setSearchParams] = useSearchParams({ q: "" }); @@ -112,4 +116,40 @@ describe("useSearchParams", () => { expect(renderer.toJSON()).toMatchSnapshot(); }); + + it("allows removal of search params when a default is provided", () => { + function SearchPage() { + let [searchParams, setSearchParams] = useSearchParams({ + value: "initial", + }); + + return ( + + The current query is "{searchParams.get("value")}". + + + ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer.toJSON()).toMatchSnapshot(); + + let button = renderer.root.findByType(Button); + + TestRenderer.act(() => { + button.props.onClick(); + }); + + expect(renderer.toJSON()).toMatchSnapshot(); + }); }); diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index d89e3fba19..07335e33ef 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -288,16 +288,19 @@ export function useSearchParams( defaultInit?: URLSearchParamsInit ): [URLSearchParams, SetURLSearchParams] { let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)); + let hasSetSearchParamsRef = React.useRef(false); let location = useLocation(); let searchParams = React.useMemo(() => { let searchParams = createSearchParams(location.search); - for (let key of defaultSearchParamsRef.current.keys()) { - if (!searchParams.has(key)) { - defaultSearchParamsRef.current.getAll(key).forEach((value) => { - searchParams.append(key, value); - }); + if (!hasSetSearchParamsRef.current) { + for (let key of defaultSearchParamsRef.current.keys()) { + if (!searchParams.has(key)) { + defaultSearchParamsRef.current.getAll(key).forEach((value) => { + searchParams.append(key, value); + }); + } } } @@ -310,6 +313,7 @@ export function useSearchParams( const newSearchParams = createSearchParams( typeof nextInit === "function" ? nextInit(searchParams) : nextInit ); + hasSetSearchParamsRef.current = true; navigate("?" + newSearchParams, navigateOpts); }, [navigate, searchParams]