Skip to content

Commit

Permalink
feat: useSearchParams supports functional updates like useState (#8955)
Browse files Browse the repository at this point in the history
* 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 <54811538+promet99@users.noreply.github.com>
  • Loading branch information
brophdawg11 and promet99 committed Jun 8, 2022
1 parent 0374930 commit 7b6feec
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 61 deletions.
4 changes: 2 additions & 2 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
- noisypigeon
- paulsmithkc
- petersendidit
- promet99
- RobHannay
- rtmann
- ryanflorence
Expand All @@ -66,6 +67,5 @@
- turansky
- underager
- vijaypushkin
- rtmann
- williamsdyyz
- vikingviolinist
- williamsdyyz
10 changes: 8 additions & 2 deletions docs/hooks/use-search-params-rn.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@ type URLSearchParamsInit =
| URLSearchParams;

type SetURLSearchParams = (
nextInit?: URLSearchParamsInit,
nextInit?:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOpts?: : NavigateOptions
) => void;

interface NavigateOptions {
replace?: boolean;
state?: any;
resetScroll?: boolean;
}
```

</details>

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.
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.

```tsx
import * as React from "react";
Expand All @@ -65,4 +68,7 @@ function App() {
}
```

[functional-updates]: https://reactjs.org/docs/hooks-reference.html#functional-updates
[searchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams
[usesearchparams]: ./use-search-params.md
[usestate]: https://reactjs.org/docs/hooks-reference.html#usestate
17 changes: 14 additions & 3 deletions docs/hooks/use-search-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ type URLSearchParamsInit =
| URLSearchParams;

type SetURLSearchParams = (
nextInit?: URLSearchParamsInit,
navigateOpts?: : { replace?: boolean; state?: any }
nextInit?:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOpts?: : NavigateOptions
) => void;

interface NavigateOptions {
replace?: boolean;
state?: any;
resetScroll?: boolean;
}
```

</details>

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.
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.

```tsx
import * as React from "react";
Expand Down Expand Up @@ -66,5 +74,8 @@ function App() {
> of the URL. Also note that the second arg to `setSearchParams` is
> the same type as the second arg to `navigate`.
[functional-updates]: https://reactjs.org/docs/hooks-reference.html#functional-updates
[searchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams
[usesearchparams-native]: ./use-search-params-rn
[usestate]: https://reactjs.org/docs/hooks-reference.html#usestate
[usenavigate]: ./use-navigate
105 changes: 83 additions & 22 deletions packages/react-router-dom/__tests__/search-params-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,6 @@ import { act } from "react-dom/test-utils";
import { MemoryRouter, Routes, Route, useSearchParams } from "react-router-dom";

describe("useSearchParams", () => {
function SearchPage() {
let queryRef = React.useRef<HTMLInputElement>(null);
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
let query = searchParams.get("q")!;

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (queryRef.current) {
setSearchParams({ q: queryRef.current.value });
}
}

return (
<div>
<p>The current query is "{query}".</p>
<form onSubmit={handleSubmit}>
<input name="q" defaultValue={query} ref={queryRef} />
</form>
</div>
);
}

let node: HTMLDivElement;
beforeEach(() => {
node = document.createElement("div");
Expand All @@ -38,6 +16,28 @@ describe("useSearchParams", () => {
});

it("reads and writes the search string", () => {
function SearchPage() {
let queryRef = React.useRef<HTMLInputElement>(null);
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
let query = searchParams.get("q")!;

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (queryRef.current) {
setSearchParams({ q: queryRef.current.value });
}
}

return (
<div>
<p>The current query is "{query}".</p>
<form onSubmit={handleSubmit}>
<input name="q" defaultValue={query} ref={queryRef} />
</form>
</div>
);
}

act(() => {
ReactDOM.render(
<MemoryRouter initialEntries={["/search?q=Michael+Jackson"]}>
Expand Down Expand Up @@ -66,4 +66,65 @@ describe("useSearchParams", () => {

expect(node.innerHTML).toMatch(/The current query is "Ryan Florence"/);
});

it("updates searchParams when a function is provided to setSearchParams (functional updates)", () => {
function SearchPage() {
let queryRef = React.useRef<HTMLInputElement>(null);
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
let query = searchParams.get("q")!;
let queryNew = searchParams.get("new")!;

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (queryRef.current) {
setSearchParams((cur) => {
cur.set("q", `${cur.get("q")} - appended`);
cur.set("new", "Ryan Florence");
return cur;
});
}
}

return (
<div>
<p>The current query is "{query}".</p>
<p>The new query is "{queryNew}"</p>
<form onSubmit={handleSubmit}>
<input name="q" defaultValue={query} ref={queryRef} />
</form>
</div>
);
}

act(() => {
ReactDOM.render(
<MemoryRouter initialEntries={["/search?q=Michael+Jackson"]}>
<Routes>
<Route path="search" element={<SearchPage />} />
</Routes>
</MemoryRouter>,
node
);
});

let form = node.querySelector("form")!;
expect(form).toBeDefined();

let queryInput = node.querySelector<HTMLInputElement>("input[name=q]")!;
expect(queryInput).toBeDefined();

expect(node.innerHTML).toMatch(/The current query is "Michael Jackson"/);
expect(node.innerHTML).toMatch(/The new query is ""/);

act(() => {
form.dispatchEvent(
new Event("submit", { bubbles: true, cancelable: true })
);
});

expect(node.innerHTML).toMatch(
/The current query is "Michael Jackson - appended"/
);
expect(node.innerHTML).toMatch(/The new query is "Ryan Florence"/);
});
});
31 changes: 20 additions & 11 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* you'll need to update the rollup config for react-router-dom-v5-compat.
*/
import * as React from "react";
import type { NavigateOptions, To } from "react-router";
import {
Router,
createPath,
Expand All @@ -16,16 +17,15 @@ import {
UNSAFE_DataRouterContext,
UNSAFE_DataRouterStateContext,
} from "react-router";
import type { To } from "react-router";
import type {
BrowserHistory,
Fetcher,
FormEncType,
FormMethod,
GetScrollRestorationKeyFunction,
HashHistory,
History,
HydrationState,
GetScrollRestorationKeyFunction,
RouteObject,
} from "@remix-run/router";
import {
Expand Down Expand Up @@ -658,7 +658,9 @@ export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
* A convenient wrapper for reading and writing search parameters via the
* URLSearchParams interface.
*/
export function useSearchParams(defaultInit?: URLSearchParamsInit) {
export function useSearchParams(
defaultInit?: URLSearchParamsInit
): [URLSearchParams, SetURLSearchParams] {
warning(
typeof URLSearchParams !== "undefined",
`You cannot use the \`useSearchParams\` hook in a browser that does not ` +
Expand All @@ -684,19 +686,26 @@ export function useSearchParams(defaultInit?: URLSearchParamsInit) {
);

let navigate = useNavigate();
let setSearchParams = React.useCallback(
(
nextInit: URLSearchParamsInit,
navigateOptions?: { replace?: boolean; state?: any }
) => {
navigate("?" + createSearchParams(nextInit), navigateOptions);
let setSearchParams = React.useCallback<SetURLSearchParams>(
(nextInit, navigateOptions) => {
const newSearchParams = createSearchParams(
typeof nextInit === "function" ? nextInit(searchParams) : nextInit
);
navigate("?" + newSearchParams, navigateOptions);
},
[navigate]
[navigate, searchParams]
);

return [searchParams, setSearchParams] as const;
return [searchParams, setSearchParams];
}

type SetURLSearchParams = (
nextInit?:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOpts?: NavigateOptions
) => void;

/**
* Submits a HTML `<form>` to the server without reloading the page.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`useSearchParams reads and writes the search string (functional update) 1`] = `
<View>
<Text>
The current query is "
Michael Jackson
".
</Text>
<Text>
The new query is "
".
</Text>
<View>
<TextInput
allowFontScaling={true}
onChangeText={[Function]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
value="Michael Jackson"
/>
</View>
</View>
`;

exports[`useSearchParams reads and writes the search string (functional update) 2`] = `
<View>
<Text>
The current query is "
Michael Jackson - appended
".
</Text>
<Text>
The new query is "
Ryan Florence
".
</Text>
<View>
<TextInput
allowFontScaling={true}
onChangeText={[Function]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
value="Michael Jackson"
/>
</View>
</View>
`;

exports[`useSearchParams reads and writes the search string 1`] = `
<View>
<Text>
Expand Down

0 comments on commit 7b6feec

Please sign in to comment.