Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
- [`useIdle`](./docs/useIdle.md) — tracks whether user is being inactive.
- [`useIntersection`](./docs/useIntersection.md) — tracks an HTML element's intersection. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-useintersection--demo)
- [`useKey`](./docs/useKey.md), [`useKeyPress`](./docs/useKeyPress.md), [`useKeyboardJs`](./docs/useKeyboardJs.md), and [`useKeyPressEvent`](./docs/useKeyPressEvent.md) — track keys. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usekeypressevent--demo)
- [`useLocation`](./docs/useLocation.md) and [`useSearchParam`](./docs/useSearchParam.md) — tracks page navigation bar location state.
- [`useLocation`](./docs/useLocation.md), [`useSearchParam`](./docs/useSearchParam.md), and [`useAllSearchParam`](./docs/useAllSearchParam.md) — tracks page navigation bar location state.
- [`useLongPress`](./docs/useLongPress.md) — tracks long press gesture of some element.
- [`useMedia`](./docs/useMedia.md) — tracks state of a CSS media query. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemedia--demo)
- [`useMediaDevices`](./docs/useMediaDevices.md) — tracks state of connected hardware devices.
Expand Down
30 changes: 30 additions & 0 deletions docs/useAllSearchParam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# `useAllSearchParam`

React sensor hook that tracks all of the browser's location search params as an object.

## Usage

```jsx
import {useAllSearchParam} from 'react-use';

const Demo = () => {
const params = useAllSearchParam();

return (
<div>
<div>params: {JSON.stringify(params)}</div>
<div>
<button onClick={() => history.pushState({}, '', location.pathname + '?edit=123&foo=bar')}>Edit post 123 (?edit=123&foo=bar)</button>
</div>
<div>
<button onClick={() => history.pushState({}, '', location.pathname)}>Clear params</button>
</div>
</div>
);
};
```

## Caveats/Gotchas

- Only the first value is kept when a param is repeated (e.g. `?foo=bar&foo=baz` yields `{foo: 'bar'}`), matching the semantics of `useSearchParam`.
- When using a hash router, like `react-router`'s [`<HashRouter>`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/HashRouter.md), this hook won't be able to read the search parameters as they are considered part of the hash of the URL by browsers.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as createMemo } from './factory/createMemo';
export { default as createReducerContext } from './factory/createReducerContext';
export { default as createReducer } from './factory/createReducer';
export { default as createStateContext } from './factory/createStateContext';
export { default as useAllSearchParam } from './useAllSearchParam';
export { default as useAsync } from './useAsync';
export { default as useAsyncFn } from './useAsyncFn';
export { default as useAsyncRetry } from './useAsyncRetry';
Expand Down
44 changes: 44 additions & 0 deletions src/useAllSearchParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from 'react';
import { isBrowser, off, on } from './misc/util';

const getAllParams = (search: string): Record<string, string> => {
const params = new URLSearchParams(search);
const result: Record<string, string> = {};

params.forEach((value, key) => {
if (!(key in result)) {
result[key] = value;
}
});

return result;
};

export type UseAllSearchParam = () => Record<string, string>;

const useAllSearchParam: UseAllSearchParam = () => {
const location = window.location;
const [params, setParams] = useState<Record<string, string>>(() => getAllParams(location.search));

useEffect(() => {
const onChange = () => {
setParams(getAllParams(location.search));
};

on(window, 'popstate', onChange);
on(window, 'pushstate', onChange);
on(window, 'replacestate', onChange);

return () => {
off(window, 'popstate', onChange);
off(window, 'pushstate', onChange);
off(window, 'replacestate', onChange);
};
}, []);

return params;
};

const useAllSearchParamServer: UseAllSearchParam = () => ({});

export default isBrowser ? useAllSearchParam : useAllSearchParamServer;
31 changes: 31 additions & 0 deletions stories/useAllSearchParam.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useAllSearchParam } from '../src';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const params = useAllSearchParam();

return (
<div>
<div>params: {JSON.stringify(params)}</div>
<div>
<button
onClick={() =>
window.history.pushState({}, '', window.location.pathname + '?edit=123&foo=bar')
}>
Edit post 123 (?edit=123&foo=bar)
</button>
</div>
<div>
<button onClick={() => window.history.pushState({}, '', window.location.pathname)}>
Clear params
</button>
</div>
</div>
);
};

storiesOf('Sensors/useAllSearchParam', module)
.add('Docs', () => <ShowDocs md={require('../docs/useAllSearchParam.md')} />)
.add('Demo', () => <Demo />);
69 changes: 69 additions & 0 deletions tests/useAllSearchParam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useAllSearchParam from '../src/useAllSearchParam';

const { location } = window;

let mockSearch: string;

beforeEach(() => {
delete (window as any).location;
const { search, ...restLocation } = location;

// @ts-ignore
window.location = { ...restLocation };

Object.defineProperty(window.location, 'search', {
get: function () {
return mockSearch;
},
});
});

it('returns all search params as an object', () => {
mockSearch = 'foo=bar&baz=quux';

const { result } = renderHook(() => useAllSearchParam());

expect(result.current).toEqual({ foo: 'bar', baz: 'quux' });
});

it('returns an empty object when there are no search params', () => {
mockSearch = '';

const { result } = renderHook(() => useAllSearchParam());

expect(result.current).toEqual({});
});

it('tracks the latest search params', () => {
mockSearch = 'foo=bar&baz=quux';

let callback;
const window$addEventListener = window.addEventListener;
window.addEventListener = (event, cb) => {
if (event === 'pushstate') {
callback = cb;
}
};

const { result } = renderHook(() => useAllSearchParam());

expect(result.current).toEqual({ foo: 'bar', baz: 'quux' });

act(() => {
mockSearch = 'foo=1&baz=2';
callback();
});

expect(result.current).toEqual({ foo: '1', baz: '2' });

window.addEventListener = window$addEventListener;
});

it('keeps the first value for duplicate keys', () => {
mockSearch = 'foo=bar&foo=baz';

const { result } = renderHook(() => useAllSearchParam());

expect(result.current).toEqual({ foo: 'bar' });
});