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]