Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,25 @@ const link = `/?${stringify(encodedQuery)}`;
<QueryParamProvider reachHistory={globalHisory}><App /></QueryParamProvider>

<QueryParamProvider history={myCustomHistory}><App /></QueryParamProvider>

// optionally specify options to query-string stringify
const stringifyOptions = { encode: false }
<QueryParamProvider ReactRouterRoute={Route} stringifyOptions={stringifyOptions}>
<App />
</QueryParamProvider>

// also accepts a transformSearchString function (searchString: string) => string
import {
ExtendedStringifyOptions,
transformSearchStringJsonSafe,
} from 'use-query-params';

const stringifyOptions: ExtendedStringifyOptions = {
transformSearchString: transformSearchStringJsonSafe,
};
<QueryParamProvider ReactRouterRoute={Route} stringifyOptions={stringifyOptions}>
<App />
</QueryParamProvider>
```

The **QueryParamProvider** component links your routing library's history to
Expand Down
2 changes: 1 addition & 1 deletion examples/react-router/src/ReadmeExample2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const UseQueryParamsExample = () => {
filters: withDefault(ArrayParam, []),
});
const { x: num, q: searchQuery, filters } = query;

console.log('got filters =', filters, num, searchQuery);
return (
<div>
<h1>num is {num}</h1>
Expand Down
46 changes: 44 additions & 2 deletions examples/react-router/src/UseQueryParamsExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as React from 'react';
import { useQueryParams, StringParam, NumberParam } from 'use-query-params';
import {
useQueryParams,
StringParam,
NumberParam,
JsonParam,
} from 'use-query-params';

const UseQueryParamsExample = () => {
const [count, setCount] = React.useState(0);
Expand All @@ -8,8 +13,9 @@ const UseQueryParamsExample = () => {
zzz: NumberParam,
test: StringParam,
anyp: StringParam,
json: JsonParam,
});
const { zzz, test, anyp } = query;
const { zzz, test, anyp, json } = query;

return (
<div className="UseQueryParamsExample">
Expand Down Expand Up @@ -84,6 +90,42 @@ const UseQueryParamsExample = () => {
</button>
</td>
</tr>
<tr>
<td>json</td>
<td>{JSON.stringify(json)}</td>
<td>{typeof json}</td>
<td>
<button
onClick={() =>
setQuery({
json: {
foo: [1, 2, 3],
bar: { abc: 'def' },
rand: 'any' + Math.floor(Math.random() * 100),
},
})
}
>
Change
</button>
<button
onClick={() =>
setQuery(
{
json: {
foo: [1, 2, 3],
bar: { abc: 'def' },
rand: 'any' + Math.floor(Math.random() * 100),
},
},
'push'
)
}
>
Change Push
</button>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
15 changes: 13 additions & 2 deletions examples/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import App from './App';
import { QueryParamProvider } from 'use-query-params';
import {
QueryParamProvider,
ExtendedStringifyOptions,
transformSearchStringJsonSafe,
} from 'use-query-params';

const queryStringifyOptions: ExtendedStringifyOptions = {
transformSearchString: transformSearchStringJsonSafe,
};

ReactDOM.render(
<Router>
<QueryParamProvider ReactRouterRoute={Route}>
<QueryParamProvider
ReactRouterRoute={Route}
stringifyOptions={queryStringifyOptions}
>
<App />
</QueryParamProvider>
</Router>,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"react-dom": ">=16.8.0"
},
"dependencies": {
"serialize-query-params": "^1.0.1"
"serialize-query-params": "^1.1.0"
},
"husky": {
"hooks": {
Expand Down
32 changes: 20 additions & 12 deletions src/LocationProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { EncodedQuery } from 'serialize-query-params';
import { EncodedQuery, ExtendedStringifyOptions } from 'serialize-query-params';

import { UrlUpdateType, HistoryLocation } from './types';
import { updateUrlQuery, createLocationWithChanges } from './updateUrlQuery';
Expand All @@ -8,15 +8,20 @@ import { updateUrlQuery, createLocationWithChanges } from './updateUrlQuery';
* Shape of the LocationProviderContext, which the hooks consume to read and
* update the URL state.
*/
type LocationProviderContext = [
() => Location,
(queryReplacements: EncodedQuery, updateType?: UrlUpdateType) => void
];
type LocationProviderContext = {
location: Location;
getLocation: () => Location;
setLocation: (
queryReplacements: EncodedQuery,
updateType?: UrlUpdateType
) => void;
};

export const LocationContext = React.createContext<LocationProviderContext>([
() => ({} as Location),
() => {},
]);
export const LocationContext = React.createContext<LocationProviderContext>({
location: {} as Location,
getLocation: () => ({} as Location),
setLocation: () => {},
});

export function useLocationContext() {
const context = React.useContext(LocationContext);
Expand All @@ -32,6 +37,7 @@ export function useLocationContext() {
type LocationProviderProps = HistoryLocation & {
/** Main app goes here */
children: React.ReactNode;
stringifyOptions?: ExtendedStringifyOptions;
};

/**
Expand All @@ -42,6 +48,7 @@ export function LocationProvider({
history,
location,
children,
stringifyOptions,
}: LocationProviderProps) {
const locationRef = React.useRef(location);
React.useEffect(() => {
Expand All @@ -58,17 +65,18 @@ export function LocationProvider({
locationRef.current = createLocationWithChanges(
queryReplacements,
locationRef.current,
updateType
updateType,
stringifyOptions
);
if (history) {
updateUrlQuery(history, locationRef.current, updateType);
}
},
[history]
[history, stringifyOptions]
);

return (
<LocationContext.Provider value={[getLocation, setLocation]}>
<LocationContext.Provider value={{ location, getLocation, setLocation }}>
{children}
</LocationContext.Provider>
);
Expand Down
37 changes: 35 additions & 2 deletions src/QueryParamProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { HistoryLocation, PushReplaceHistory } from './types';
import { LocationProvider } from './LocationProvider';
import { ExtendedStringifyOptions } from 'serialize-query-params';
import shallowEqual from './shallowEqual';

// we use a lazy caching solution to prevent #46 from happening
let cachedWindowHistory: History | undefined;
Expand Down Expand Up @@ -117,6 +119,7 @@ export function getLocationProps({
* Props for the Provider component, used to hook the active routing
* system into our controls.
*/

interface QueryParamProviderProps {
/** Main app goes here */
children: React.ReactNode;
Expand All @@ -131,6 +134,14 @@ interface QueryParamProviderProps {
* location provided by the active routing system is used.
*/
location?: Location;

/**
* Options to customize the stringifying of the query string
* These are passed directly to query-string.stringify, with
* the exception of transformSearchString which runs on the result
* of stringify if provided.
*/
stringifyOptions?: ExtendedStringifyOptions;
}

/**
Expand All @@ -143,14 +154,32 @@ export function QueryParamProvider({
reachHistory,
history,
location,
stringifyOptions,
}: QueryParamProviderProps) {
// cache the stringify options object so we users can just do
// <QueryParamProvider stringifyOptions={{ encode: false }} />
const stringifyOptionsRef = React.useRef(stringifyOptions);
const hasNewStringifyOptions = !shallowEqual(
stringifyOptionsRef.current,
stringifyOptions
);
const stringifyOptionsCached = hasNewStringifyOptions
? stringifyOptions
: stringifyOptionsRef.current;
React.useEffect(() => {
stringifyOptionsRef.current = stringifyOptionsCached;
}, [stringifyOptionsCached]);

// if we have React Router, use it to get the context value
if (ReactRouterRoute) {
return (
<ReactRouterRoute>
{(routeProps: any) => {
return (
<LocationProvider {...getLocationProps(routeProps)}>
<LocationProvider
stringifyOptions={stringifyOptionsCached}
{...getLocationProps(routeProps)}
>
{children}
</LocationProvider>
);
Expand All @@ -163,6 +192,7 @@ export function QueryParamProvider({
if (reachHistory) {
return (
<LocationProvider
stringifyOptions={stringifyOptionsCached}
{...getLocationProps({
history: adaptReachHistory(reachHistory),
location,
Expand All @@ -175,7 +205,10 @@ export function QueryParamProvider({

// neither reach nor react-router, so allow manual overrides
return (
<LocationProvider {...getLocationProps({ history, location })}>
<LocationProvider
stringifyOptions={stringifyOptionsCached}
{...getLocationProps({ history, location })}
>
{children}
</LocationProvider>
);
Expand Down
8 changes: 5 additions & 3 deletions src/updateUrlQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
EncodedQuery,
updateLocation,
updateInLocation,
ExtendedStringifyOptions,
} from 'serialize-query-params';
import { PushReplaceHistory, UrlUpdateType } from './types';

Expand All @@ -15,16 +16,17 @@ import { PushReplaceHistory, UrlUpdateType } from './types';
export function createLocationWithChanges(
queryReplacements: EncodedQuery,
location: Location,
updateType: UrlUpdateType = 'pushIn'
updateType: UrlUpdateType = 'pushIn',
stringifyOptions?: ExtendedStringifyOptions
): Location {
switch (updateType) {
case 'replace':
case 'push':
return updateLocation(queryReplacements, location);
return updateLocation(queryReplacements, location, stringifyOptions);
case 'replaceIn':
case 'pushIn':
default:
return updateInLocation(queryReplacements, location);
return updateInLocation(queryReplacements, location, stringifyOptions);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/useQueryParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type NewValueType<D> = D | ((latestValue: D) => D);
* Abstracted into its own function to allow re-use in a functional setter (#26)
*/
function getLatestDecodedValue<D, D2 = D>(
getLocation: () => Location,
location: Location,
name: string,
paramConfig: QueryParamConfig<D, D2>,
paramConfigRef: React.MutableRefObject<QueryParamConfig<D, D2>>,
Expand All @@ -27,7 +27,7 @@ function getLatestDecodedValue<D, D2 = D>(

// read in the parsed query
const parsedQuery = sharedMemoizedQueryParser(
getSSRSafeSearchString(getLocation()) // get the latest location object
getSSRSafeSearchString(location) // get the latest location object
);

// read in the encoded string value (we have to check cache if available because
Expand Down Expand Up @@ -85,11 +85,11 @@ export const useQueryParam = <D, D2 = D>(
D2 | undefined,
(newValue: NewValueType<D>, updateType?: UrlUpdateType) => void
] => {
const [getLocation, setLocation] = useLocationContext();
const { location, getLocation, setLocation } = useLocationContext();

// read in the raw query
const parsedQuery = sharedMemoizedQueryParser(
getSSRSafeSearchString(getLocation())
getSSRSafeSearchString(location)
);

// make caches
Expand All @@ -98,7 +98,7 @@ export const useQueryParam = <D, D2 = D>(
const decodedValueCacheRef = React.useRef<D2 | undefined>();

const decodedValue = getLatestDecodedValue(
getLocation,
location,
name,
paramConfig,
paramConfigRef,
Expand All @@ -123,7 +123,7 @@ export const useQueryParam = <D, D2 = D>(
if (typeof newValue === 'function') {
// get latest decoded value to pass as a fresh arg to the setter fn
const latestValue = getLatestDecodedValue(
getLocation,
getLocation(),
name,
paramConfig,
paramConfigRef,
Expand Down
Loading