Skip to content

Commit 7b6feec

Browse files
feat: useSearchParams supports functional updates like useState (#8955)
* Feat: useSearchParams supports functional updates like React.useState (#8909) * [Feat] add functional update to useSearchParams * add contributor * add update for react native * docs Co-authored-by: Chris Park <[email protected]>
1 parent 0374930 commit 7b6feec

File tree

8 files changed

+247
-61
lines changed

8 files changed

+247
-61
lines changed

contributors.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- noisypigeon
5050
- paulsmithkc
5151
- petersendidit
52+
- promet99
5253
- RobHannay
5354
- rtmann
5455
- ryanflorence
@@ -66,6 +67,5 @@
6667
- turansky
6768
- underager
6869
- vijaypushkin
69-
- rtmann
70-
- williamsdyyz
7170
- vikingviolinist
71+
- williamsdyyz

docs/hooks/use-search-params-rn.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,22 @@ type URLSearchParamsInit =
2626
| URLSearchParams;
2727

2828
type SetURLSearchParams = (
29-
nextInit?: URLSearchParamsInit,
29+
nextInit?:
30+
| URLSearchParamsInit
31+
| ((prev: URLSearchParams) => URLSearchParamsInit),
3032
navigateOpts?: : NavigateOptions
3133
) => void;
3234

3335
interface NavigateOptions {
3436
replace?: boolean;
3537
state?: any;
38+
resetScroll?: boolean;
3639
}
3740
```
3841

3942
</details>
4043

41-
The `useSearchParams` hook is used to read and modify the query string in the URL for the current location. Like React's own [`useState` hook](https://reactjs.org/docs/hooks-reference.html#usestate), `useSearchParams` returns an array of two values: the current location's [search params](https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams) and a function that may be used to update them.
44+
The `useSearchParams` hook is used to read and modify the query string in the URL for the current location. Like React's own [`useState` hook][usestate], `useSearchParams` returns an array of two values: the current location's [search params][searchparams] and a function that may be used to update them. Just as React's [`useState` hook][usestate], `setSearchParams` also supports [functional updates][functional-updates]. Therefore, you may provide a function that takes a `searchParams` and returns an updated version.
4245

4346
```tsx
4447
import * as React from "react";
@@ -65,4 +68,7 @@ function App() {
6568
}
6669
```
6770

71+
[functional-updates]: https://reactjs.org/docs/hooks-reference.html#functional-updates
72+
[searchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams
6873
[usesearchparams]: ./use-search-params.md
74+
[usestate]: https://reactjs.org/docs/hooks-reference.html#usestate

docs/hooks/use-search-params.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,22 @@ type URLSearchParamsInit =
2626
| URLSearchParams;
2727

2828
type SetURLSearchParams = (
29-
nextInit?: URLSearchParamsInit,
30-
navigateOpts?: : { replace?: boolean; state?: any }
29+
nextInit?:
30+
| URLSearchParamsInit
31+
| ((prev: URLSearchParams) => URLSearchParamsInit),
32+
navigateOpts?: : NavigateOptions
3133
) => void;
34+
35+
interface NavigateOptions {
36+
replace?: boolean;
37+
state?: any;
38+
resetScroll?: boolean;
39+
}
3240
```
3341

3442
</details>
3543

36-
The `useSearchParams` hook is used to read and modify the query string in the URL for the current location. Like React's own [`useState` hook](https://reactjs.org/docs/hooks-reference.html#usestate), `useSearchParams` returns an array of two values: the current location's [search params](https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams) and a function that may be used to update them.
44+
The `useSearchParams` hook is used to read and modify the query string in the URL for the current location. Like React's own [`useState` hook][usestate], `useSearchParams` returns an array of two values: the current location's [search params][searchparams] and a function that may be used to update them. Just as React's [`useState` hook][usestate], `setSearchParams` also supports [functional updates][functional-updates]. Therefore, you may provide a function that takes a `searchParams` and returns an updated version.
3745

3846
```tsx
3947
import * as React from "react";
@@ -66,5 +74,8 @@ function App() {
6674
> of the URL. Also note that the second arg to `setSearchParams` is
6775
> the same type as the second arg to `navigate`.
6876
77+
[functional-updates]: https://reactjs.org/docs/hooks-reference.html#functional-updates
78+
[searchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams
6979
[usesearchparams-native]: ./use-search-params-rn
80+
[usestate]: https://reactjs.org/docs/hooks-reference.html#usestate
7081
[usenavigate]: ./use-navigate

packages/react-router-dom/__tests__/search-params-test.tsx

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,6 @@ import { act } from "react-dom/test-utils";
44
import { MemoryRouter, Routes, Route, useSearchParams } from "react-router-dom";
55

66
describe("useSearchParams", () => {
7-
function SearchPage() {
8-
let queryRef = React.useRef<HTMLInputElement>(null);
9-
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
10-
let query = searchParams.get("q")!;
11-
12-
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
13-
event.preventDefault();
14-
if (queryRef.current) {
15-
setSearchParams({ q: queryRef.current.value });
16-
}
17-
}
18-
19-
return (
20-
<div>
21-
<p>The current query is "{query}".</p>
22-
<form onSubmit={handleSubmit}>
23-
<input name="q" defaultValue={query} ref={queryRef} />
24-
</form>
25-
</div>
26-
);
27-
}
28-
297
let node: HTMLDivElement;
308
beforeEach(() => {
319
node = document.createElement("div");
@@ -38,6 +16,28 @@ describe("useSearchParams", () => {
3816
});
3917

4018
it("reads and writes the search string", () => {
19+
function SearchPage() {
20+
let queryRef = React.useRef<HTMLInputElement>(null);
21+
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
22+
let query = searchParams.get("q")!;
23+
24+
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
25+
event.preventDefault();
26+
if (queryRef.current) {
27+
setSearchParams({ q: queryRef.current.value });
28+
}
29+
}
30+
31+
return (
32+
<div>
33+
<p>The current query is "{query}".</p>
34+
<form onSubmit={handleSubmit}>
35+
<input name="q" defaultValue={query} ref={queryRef} />
36+
</form>
37+
</div>
38+
);
39+
}
40+
4141
act(() => {
4242
ReactDOM.render(
4343
<MemoryRouter initialEntries={["/search?q=Michael+Jackson"]}>
@@ -66,4 +66,65 @@ describe("useSearchParams", () => {
6666

6767
expect(node.innerHTML).toMatch(/The current query is "Ryan Florence"/);
6868
});
69+
70+
it("updates searchParams when a function is provided to setSearchParams (functional updates)", () => {
71+
function SearchPage() {
72+
let queryRef = React.useRef<HTMLInputElement>(null);
73+
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
74+
let query = searchParams.get("q")!;
75+
let queryNew = searchParams.get("new")!;
76+
77+
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
78+
event.preventDefault();
79+
if (queryRef.current) {
80+
setSearchParams((cur) => {
81+
cur.set("q", `${cur.get("q")} - appended`);
82+
cur.set("new", "Ryan Florence");
83+
return cur;
84+
});
85+
}
86+
}
87+
88+
return (
89+
<div>
90+
<p>The current query is "{query}".</p>
91+
<p>The new query is "{queryNew}"</p>
92+
<form onSubmit={handleSubmit}>
93+
<input name="q" defaultValue={query} ref={queryRef} />
94+
</form>
95+
</div>
96+
);
97+
}
98+
99+
act(() => {
100+
ReactDOM.render(
101+
<MemoryRouter initialEntries={["/search?q=Michael+Jackson"]}>
102+
<Routes>
103+
<Route path="search" element={<SearchPage />} />
104+
</Routes>
105+
</MemoryRouter>,
106+
node
107+
);
108+
});
109+
110+
let form = node.querySelector("form")!;
111+
expect(form).toBeDefined();
112+
113+
let queryInput = node.querySelector<HTMLInputElement>("input[name=q]")!;
114+
expect(queryInput).toBeDefined();
115+
116+
expect(node.innerHTML).toMatch(/The current query is "Michael Jackson"/);
117+
expect(node.innerHTML).toMatch(/The new query is ""/);
118+
119+
act(() => {
120+
form.dispatchEvent(
121+
new Event("submit", { bubbles: true, cancelable: true })
122+
);
123+
});
124+
125+
expect(node.innerHTML).toMatch(
126+
/The current query is "Michael Jackson - appended"/
127+
);
128+
expect(node.innerHTML).toMatch(/The new query is "Ryan Florence"/);
129+
});
69130
});

packages/react-router-dom/index.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* you'll need to update the rollup config for react-router-dom-v5-compat.
44
*/
55
import * as React from "react";
6+
import type { NavigateOptions, To } from "react-router";
67
import {
78
Router,
89
createPath,
@@ -16,16 +17,15 @@ import {
1617
UNSAFE_DataRouterContext,
1718
UNSAFE_DataRouterStateContext,
1819
} from "react-router";
19-
import type { To } from "react-router";
2020
import type {
2121
BrowserHistory,
2222
Fetcher,
2323
FormEncType,
2424
FormMethod,
25+
GetScrollRestorationKeyFunction,
2526
HashHistory,
2627
History,
2728
HydrationState,
28-
GetScrollRestorationKeyFunction,
2929
RouteObject,
3030
} from "@remix-run/router";
3131
import {
@@ -658,7 +658,9 @@ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
658658
* A convenient wrapper for reading and writing search parameters via the
659659
* URLSearchParams interface.
660660
*/
661-
export function useSearchParams(defaultInit?: URLSearchParamsInit) {
661+
export function useSearchParams(
662+
defaultInit?: URLSearchParamsInit
663+
): [URLSearchParams, SetURLSearchParams] {
662664
warning(
663665
typeof URLSearchParams !== "undefined",
664666
`You cannot use the \`useSearchParams\` hook in a browser that does not ` +
@@ -684,19 +686,26 @@ export function useSearchParams(defaultInit?: URLSearchParamsInit) {
684686
);
685687

686688
let navigate = useNavigate();
687-
let setSearchParams = React.useCallback(
688-
(
689-
nextInit: URLSearchParamsInit,
690-
navigateOptions?: { replace?: boolean; state?: any }
691-
) => {
692-
navigate("?" + createSearchParams(nextInit), navigateOptions);
689+
let setSearchParams = React.useCallback<SetURLSearchParams>(
690+
(nextInit, navigateOptions) => {
691+
const newSearchParams = createSearchParams(
692+
typeof nextInit === "function" ? nextInit(searchParams) : nextInit
693+
);
694+
navigate("?" + newSearchParams, navigateOptions);
693695
},
694-
[navigate]
696+
[navigate, searchParams]
695697
);
696698

697-
return [searchParams, setSearchParams] as const;
699+
return [searchParams, setSearchParams];
698700
}
699701

702+
type SetURLSearchParams = (
703+
nextInit?:
704+
| URLSearchParamsInit
705+
| ((prev: URLSearchParams) => URLSearchParamsInit),
706+
navigateOpts?: NavigateOptions
707+
) => void;
708+
700709
/**
701710
* Submits a HTML `<form>` to the server without reloading the page.
702711
*/

packages/react-router-native/__tests__/__snapshots__/search-params-test.tsx.snap

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`useSearchParams reads and writes the search string (functional update) 1`] = `
4+
<View>
5+
<Text>
6+
The current query is "
7+
Michael Jackson
8+
".
9+
</Text>
10+
<Text>
11+
The new query is "
12+
".
13+
</Text>
14+
<View>
15+
<TextInput
16+
allowFontScaling={true}
17+
onChangeText={[Function]}
18+
rejectResponderTermination={true}
19+
underlineColorAndroid="transparent"
20+
value="Michael Jackson"
21+
/>
22+
</View>
23+
</View>
24+
`;
25+
26+
exports[`useSearchParams reads and writes the search string (functional update) 2`] = `
27+
<View>
28+
<Text>
29+
The current query is "
30+
Michael Jackson - appended
31+
".
32+
</Text>
33+
<Text>
34+
The new query is "
35+
Ryan Florence
36+
".
37+
</Text>
38+
<View>
39+
<TextInput
40+
allowFontScaling={true}
41+
onChangeText={[Function]}
42+
rejectResponderTermination={true}
43+
underlineColorAndroid="transparent"
44+
value="Michael Jackson"
45+
/>
46+
</View>
47+
</View>
48+
`;
49+
350
exports[`useSearchParams reads and writes the search string 1`] = `
451
<View>
552
<Text>

0 commit comments

Comments
 (0)