use-query-params
Version:
React Hook for managing state in URL query parameters with easy serialization.
654 lines (502 loc) • 21.5 kB
Markdown
<div align="center">
<h1>useQueryParams</h1>
<p>A <a href="https://reactjs.org/docs/hooks-intro.html">React Hook</a>, HOC, and Render Props solution for managing state in URL query parameters with easy serialization.
</p>
<p>Works with <a href="https://reacttraining.com/react-router/">React Router</a> out of the box. TypeScript supported.</p>
<p>
<a href="https://www.npmjs.com/package/use-query-params"><img alt="npm" src="https://img.shields.io/npm/v/use-query-params.svg"></a>
<a href="https://travis-ci.com/pbeshai/use-query-params/"><img alt="Travis (.com)" src="https://img.shields.io/travis/com/pbeshai/use-query-params.svg"></a>
</p>
<hr />
<a href="#installation">Installation</a> |
<a href="#usage">Usage</a> |
<a href="#examples">Examples</a> |
<a href="#api">API</a> |
<a href="https://pbeshai.github.io/use-query-params/">Demo</a>
</div>
<hr/>
When creating apps with easily shareable URLs, you often want to encode state as query parameters, but all query parameters must be encoded as strings. `useQueryParams` allows you to easily encode and decode data of any type as query parameters with smart memoization to prevent creating unnecessary duplicate objects. It uses [serialize-query-params](../serialize-query-params/).
**Migrating from v1?** See [details in the changelog](./CHANGELOG.md#migrating-from-v1).
### Installation
Using npm:
```
$ npm install --save use-query-params
```
Link your routing system via an adapter (e.g., [React Router 6 example](../../examples/react-router-6/src/index.tsx), [React Router 5 example](../../examples/react-router-5/src/index.tsx)):
```js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<BrowserRouter>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<Routes>
<Route path="/" element={<App />}>
</Routes>
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);
```
By default, use-query-params uses [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) to handle interpreting the location string, which means it does not decode `null` and has limited handling of other more advanced URL parameter configurations. If you want access to those features, add a third-party library like [query-string](https://github.com/sindresorhus/query-string) and tell use-query-params to use it:
```js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
// optionally use the query-string parse / stringify functions to
// handle more advanced cases than URLSearchParams supports.
import { parse, stringify } from 'query-string';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<BrowserRouter>
<QueryParamProvider
adapter={ReactRouter6Adapter}
options={{
searchStringToObject: parse,
objectToSearchString: stringify,
}}
>
<Routes>
<Route path="/" element={<App />}>
</Routes>
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);
```
### Usage
Be sure to add **QueryParamProvider** as shown in Installation above.
Add the hook to your component. There are two options: `useQueryParam`:
```js
import * as React from 'react';
import { useQueryParam, NumberParam, StringParam } from 'use-query-params';
const UseQueryParamExample = () => {
// something like: ?x=123&foo=bar in the URL
const [num, setNum] = useQueryParam('x', NumberParam);
const [foo, setFoo] = useQueryParam('foo', StringParam);
return (
<div>
<h1>num is {num}</h1>
<button onClick={() => setNum(Math.random())}>Change</button>
<h1>foo is {foo}</h1>
<button onClick={() => setFoo(`str${Math.random()}`)}>Change</button>
</div>
);
};
export default UseQueryParamExample;
```
Or `useQueryParams`:
```js
import * as React from 'react';
import {
useQueryParams,
StringParam,
NumberParam,
ArrayParam,
withDefault,
} from 'use-query-params';
// create a custom parameter with a default value
const MyFiltersParam = withDefault(ArrayParam, [])
const UseQueryParamsExample = () => {
// something like: ?x=123&q=foo&filters=a&filters=b&filters=c in the URL
const [query, setQuery] = useQueryParams({
x: NumberParam,
q: StringParam,
filters: MyFiltersParam,
});
const { x: num, q: searchQuery, filters } = query;
return (
<div>
<h1>num is {num}</h1>
<button onClick={() => setQuery({ x: Math.random() })}>Change</button>
<h1>searchQuery is {searchQuery}</h1>
<h1>There are {filters.length} filters active.</h1>
<button
onClick={() =>
setQuery(
{ x: Math.random(), filters: [...filters, 'foo'], q: 'bar' },
'push'
)
}
>
Change All
</button>
</div>
);
};
export default UseQueryParamsExample;
```
<details>
<summary>Or with the higher-order component (HOC) <b>withQueryParams</b>:</summary>
```js
import * as React from 'react';
import {
withQueryParams,
StringParam,
NumberParam,
ArrayParam,
withDefault,
} from 'use-query-params';
const WithQueryParamsExample = ({ query, setQuery }: any) => {
const { x: num, q: searchQuery, filters } = query;
return (
<div>
<h1>num is {num}</h1>
<button onClick={() => setQuery({ x: Math.random() })}>Change</button>
<h1>searchQuery is {searchQuery}</h1>
<h1>There are {filters.length} filters active.</h1>
<button
onClick={() =>
setQuery(
{ x: Math.random(), filters: [...filters, 'foo'], q: 'bar' },
'push'
)
}
>
Change All
</button>
</div>
);
};
// create a custom parameter with a default value
const MyFiltersParam = withDefault(ArrayParam, [])
export default withQueryParams({
x: NumberParam,
q: StringParam,
filters: MyFiltersParam,
}, WithQueryParamsExample);
```
</details>
<details>
<summary>Or with render props via <b><QueryParams></b>:</summary>
```js
import * as React from 'react';
import {
QueryParams,
StringParam,
NumberParam,
ArrayParam,
withDefault,
} from 'use-query-params';
// create a custom parameter with a default value
const MyFiltersParam = withDefault(ArrayParam, [])
const RenderPropsExample = () => {
const queryConfig = {
x: NumberParam,
q: StringParam,
filters: MyFiltersParam,
};
return (
<div>
<QueryParams config={queryConfig}>
{({ query, setQuery }) => {
const { x: num, q: searchQuery, filters } = query;
return (
<>
<h1>num is {num}</h1>
<button onClick={() => setQuery({ x: Math.random() })}>
Change
</button>
<h1>searchQuery is {searchQuery}</h1>
<h1>There are {filters.length} filters active.</h1>
<button
onClick={() =>
setQuery(
{
x: Math.random(),
filters: [...filters, 'foo'],
q: 'bar',
},
'push'
)
}
>
Change All
</button>
</>
);
}}
</QueryParams>
</div>
);
};
export default RenderPropsExample;
```
</details>
### Examples
A few basic [examples](../../examples) have been put together to demonstrate how `useQueryParams` works with different routing systems.
- [React Router 6 Example](../../examples/react-router-6)
- [React Router 5 Example](../../examples/react-router-5)
### API
- [UrlUpdateType](#urlupdatetype)
- [Param Types](#param-types)
- [useQueryParam](#usequeryparam)
- [useQueryParams](#usequeryparams-1)
- [withQueryParams](#withqueryparams)
- [QueryParams](#queryparams)
- [encodeQueryParams](#encodequeryparams)
- [QueryParamProvider](#queryparamprovider)
- [Type Definitions](./src/types.ts) and from [serialize-query-params](../serialize-query-params/src/types.ts).
- [Serialization Utility Functions](../serialize-query-params/src/serialize.ts)
For convenience, use-query-params exports all of the [serialize-query-params](../serialize-query-params) library.
#### UrlUpdateType
The `UrlUpdateType` is a string type definings the different methods for updating the URL:
- `'pushIn'`: Push just a single parameter, leaving the rest as is (back button works) (the default)
- `'push'`: Push all parameters with just those specified (back button works)
- `'replaceIn'`: Replace just a single parameter, leaving the rest as is
- `'replace'`: Replace all parameters with just those specified
#### Param Types
See [all param definitions from serialize-query-params here](../serialize-query-params/src/params.ts). You can define your own parameter types by creating an object with an `encode` and a `decode` function. See the existing definitions for examples.
Note that all null and empty values are typically treated as follows:
Note that with the default searchStringToObject implementation that uses URLSearchParams, null and empty values are treated as follows:
| value | encoding |
| --- | --- |
| `""` | `?qp=` |
| `null` | `?` (removed from URL) |
| `undefined` | `?` (removed from URL) |
If you need a more discerning interpretation, you can use [query-string](https://github.com/sindresorhus/query-string)'s parse and stringify to get:
| value | encoding |
| --- | --- |
| `""` | `?qp=` |
| `null` | `?qp` |
| `undefined` | `?` (removed from URL) |
Examples in this table assume query parameter named `qp`.
| Param | Type | Example Decoded | Example Encoded |
| --- | --- | --- | --- |
| StringParam | string | `'foo'` | `?qp=foo` |
| NumberParam | number | `123` | `?qp=123` |
| ObjectParam | { key: string } | `{ foo: 'bar', baz: 'zzz' }` | `?qp=foo-bar_baz-zzz` |
| ArrayParam | string[] | `['a','b','c']` | `?qp=a&qp=b&qp=c` |
| JsonParam | any | `{ foo: 'bar' }` | `?qp=%7B%22foo%22%3A%22bar%22%7D` |
| DateParam | Date | `Date(2019, 2, 1)` | `?qp=2019-03-01` |
| BooleanParam | boolean | `true` | `?qp=1` |
| NumericObjectParam | { key: number } | `{ foo: 1, bar: 2 }` | `?qp=foo-1_bar-2` |
| DelimitedArrayParam | string[] | `['a','b','c']` | `?qp=a_b_c` |
| DelimitedNumericArrayParam | number[] | `[1, 2, 3]` | `?qp=1_2_3` |
**Example**
```js
import { ArrayParam, useQueryParam, useQueryParams } from 'use-query-params';
// typically used with the hooks:
const [foo, setFoo] = useQueryParam('foo', ArrayParam);
// - OR -
const [query, setQuery] = useQueryParams({ foo: ArrayParam });
```
**Example with Custom Param**
You can define your own params if the ones shipped with this package don't work for you. There are included [serialization utility functions](../serialize-query-params/src/serialize.ts) to make this easier, but you can use whatever you like.
```js
import {
encodeDelimitedArray,
decodeDelimitedArray
} from 'use-query-params';
/** Uses a comma to delimit entries. e.g. ['a', 'b'] => qp?=a,b */
const CommaArrayParam = {
encode: (array: string[] | null | undefined) =>
encodeDelimitedArray(array, ','),
decode: (arrayStr: string | string[] | null | undefined) =>
decodeDelimitedArray(arrayStr, ',')
};
```
<br/>
#### useQueryParam
```js
useQueryParam<T>(name: string, paramConfig?: QueryParamConfig<T>, options?: QueryParamOptions):
[T | undefined, (newValue: T, updateType?: UrlUpdateType) => void]
```
Given a query param name and query parameter configuration `{ encode, decode }`
return the decoded value and a setter for updating it. If you do not provide a paramConfig, it inherits it from what was defined in the QueryParamProvider, falling back to StringParam if nothing is found.
The setter takes two arguments `(newValue, updateType)` where updateType
is one of `'pushIn' | 'push' | 'replaceIn' | 'replace'`, defaulting to
`'pushIn'`.
You can override options from the QueryParamProvider with the third argument. See [QueryParamOptions](#queryparamoptions) for details.
**Example**
```js
import { useQueryParam, NumberParam } from 'use-query-params';
// reads query parameter `foo` from the URL and stores its decoded numeric value
const [foo, setFoo] = useQueryParam('foo', NumberParam);
setFoo(500);
setFoo(123, 'push');
// to unset or remove a parameter set it to undefined and use pushIn or replaceIn update types
setFoo(undefined) // ?foo=123&bar=zzz becomes ?bar=zzz
// functional updates are also supported:
setFoo((latestFoo) => latestFoo + 150)
```
<br/>
#### useQueryParams
```js
// option 1: pass only a config with possibly some options
useQueryParams<QPCMap extends QueryParamConfigMapWithInherit>(
paramConfigMap: QPCMap,
options?: QueryParamOptions
): [DecodedValueMap<QPCMap>, SetQuery<QPCMap>];
// option 2: pass an array of param names, relying on predefined params for types
useQueryParams<QPCMap extends QueryParamConfigMapWithInherit>(
names: string[],
options?: QueryParamOptions
): [DecodedValueMap<QPCMap>, SetQuery<QPCMap>];
// option 3: pass no args, get all params back that were predefined in a proivder
useQueryParams<QPCMap extends QueryParamConfigMapWithInherit>(
): [DecodedValueMap<QPCMap>, SetQuery<QPCMap>];
```
Given a query parameter configuration (mapping query param name to `{ encode, decode }`),
return an object with the decoded values and a setter for updating them.
The setter takes two arguments `(newQuery, updateType)` where updateType
is one of `'pushIn' | 'push' | 'replaceIn' | 'replace'`, defaulting to
`'pushIn'`.
You can override options from the QueryParamProvider with the options argument. See [QueryParamOptions](#queryparamoptions) for details.
**Example**
```js
import { useQueryParams, StringParam, NumberParam } from 'use-query-params';
// reads query parameters `foo` and `bar` from the URL and stores their decoded values
const [query, setQuery] = useQueryParams({ foo: NumberParam, bar: StringParam });
setQuery({ foo: 500 })
setQuery({ foo: 123, bar: 'zzz' }, 'push');
// to unset or remove a parameter set it to undefined and use pushIn or replaceIn update types
setQuery({ foo: undefined }) // ?foo=123&bar=zzz becomes ?bar=zzz
// functional updates are also supported:
setQuery((latestQuery) => ({ foo: latestQuery.foo + 150 }))
```
**Example with Custom Parameter Type**
Parameter types are just objects with `{ encode, decode }` functions. You can
provide your own if the provided ones don't work for your use case.
```js
import { useQueryParams } from 'use-query-params';
const MyParam = {
encode(value) {
return `${value * 10000}`;
},
decode(strValue) {
return parseFloat(strValue) / 10000;
}
}
// ?foo=10000 -> query = { foo: 1 }
const [query, setQuery] = useQueryParams({ foo: MyParam });
// goes to ?foo=99000
setQuery({ foo: 99 })
```
<br/>
#### withQueryParams
```js
withQueryParams<QPCMap extends QueryParamConfigMap, P extends InjectedQueryProps<QPCMap>>
(paramConfigMap: QPCMap, WrappedComponent: React.ComponentType<P>):
React.FC<Diff<P, InjectedQueryProps<QPCMap>>>
```
Given a query parameter configuration (mapping query param name to `{ encode, decode }`) and
a component, inject the props `query` and `setQuery` into the component based on the config.
The setter takes two arguments `(newQuery, updateType)` where updateType
is one of `'pushIn' | 'push' | 'replaceIn' | 'replace'`, defaulting to
`'pushIn'`.
**Example**
```js
import { withQueryParams, StringParam, NumberParam } from 'use-query-params';
const MyComponent = ({ query, setQuery, ...others }) => {
const { foo, bar } = query;
return <div>foo = {foo}, bar = {bar}</div>
}
// reads query parameters `foo` and `bar` from the URL and stores their decoded values
export default withQueryParams({ foo: NumberParam, bar: StringParam }, MyComponent);
```
Note there is also a variant called `withQueryParamsMapped` that allows you to do a react-redux style mapStateToProps equivalent. See [the code](./src/withQueryParams.tsx#L51) or [this example](../../examples/react-router/src/ReadmeExample3Mapped.tsx) for details.
<br/>
#### QueryParams
```js
<QueryParams config={{ foo: NumberParam }}>
{({ query, setQuery }) => <div>foo = {query.foo}</div>}
</QueryParams>
```
Given a query parameter configuration (mapping query param name to `{ encode, decode }`) and
a component, provide render props `query` and `setQuery` based on the config.
The setter takes two arguments `(newQuery, updateType)` where updateType
is one of `'pushIn' | 'push' | 'replaceIn' | 'replace'`, defaulting to
`'pushIn'`.
<br/>
#### encodeQueryParams
```js
encodeQueryParams<QPCMap extends QueryParamConfigMap>(
paramConfigMap: QPCMap,
query: Partial<DecodedValueMap<QPCMap>>
): EncodedQueryWithNulls
```
Convert the values in query to strings via the encode functions configured
in paramConfigMap. This can be useful for constructing links using decoded
query parameters.
**Example**
```js
import { encodeQueryParams, NumberParam } from 'use-query-params';
// since v1.0 stringify is not exported from 'use-query-params',
// so you must install the 'query-string' package in case you need it
import { stringify } from 'query-string';
// encode each parameter according to the configuration
const encodedQuery = encodeQueryParams({ foo: NumberParam }, { foo });
const link = `/?${stringify(encodedQuery)}`;
```
<br/>
#### QueryParamProvider
```js
// choose an adapter, depending on your router
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
<QueryParamProvider adapter={ReactRouter6Adapter}><App /></QueryParamProvider>
// optionally specify options
import { parse, stringify } from 'query-string';
const options = {
searchStringToObject: parse,
objectToSearchString: stringify,
}
<QueryParamProvider adapter={ReactRouter6Adapter} options={options}>
<App />
</QueryParamProvider>
// optionally nest parameters in options to get automatically on useQueryParams calls
<QueryParamProvider adapter={ReactRouter6Adapter} options={{
params: { foo: NumberParam }
}}>
<App> {/* useQueryParams calls have access to foo here */}
...
<Page1>
<QueryParamProvider options={{ params: { bar: BooleanParam }}}>
... {/* useQueryParams calls have access to foo and bar here */}
</QueryParamProvider>
</Page1>
...
</App>
</QueryParamProvider>
```
The **QueryParamProvider** component links your routing library's history to
the **useQueryParams** hook. It is needed for the hook to be able to update
the URL and have the rest of your app know about it.
You can specify global options at the provider level.
##### QueryParamOptions
| option | default | description |
| --- | --- | --- |
| updateType | "pushIn" | How the URL gets updated by default, one of: replace, replaceIn, push, pushIn. |
| searchStringToObject | from serialize-query-params | How to convert the search string e.g. `?foo=123&bar=x` into an object. Default uses URLSearchParams, but you could also use `parse` from query-string for example. `(searchString: string) => Record<string, string | (string | null)[] | null | undefined>` |
| objectToSearchString | from serialize-query-params | How to convert an object (e.g. `{ foo: 'x' }` -> `foo=x` into a search string – no "?" included). Default uses URLSearchParams, but you could also use `stringify` from query-string for example. `(query: Record<string, string | (string | null)[] | null | undefined>) => string` |
| params | undefined | Define parameters at the provider level to be automatically available to hook calls. Type is QueryParamConfigMap, e.g. `{ params: { foo: NumberParam, bar: BooleanParam }}`
| includeKnownParams | undefined | When true, include all parameters that were configured via the `params` option on a QueryParamProvider. Default behavior depends on the arguments passed to useQueryParams (if not specifying any params, it is true, otherwise false). |
| includeAllParams | false | Include all parameters found in the URL even if not configured in any param config. |
| removeDefaultsFromUrl | false | When true, removes parameters from the URL when set is called if their value is the same as their default (based on the `default` attribute of the Param object, typically populated by `withDefault()`) |
| *enableBatching* | false | **experimental** - turns on batching (i.e., multiple consecutive calls to setQueryParams in a row only result in a single update to the URL). Currently marked as experimental since we need to update all the tests to verify no issues occur, feedback welcome. |
<br/>
### Development
Run the typescript compiler in watch mode:
```
npm run dev
```
You can run an example app:
```
npm link
cd examples/react-router
npm install
npm link use-query-params
npm start
```