diff --git a/README.md b/README.md index cc1728fafd..9eea9c9417 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/useAllSearchParam.md b/docs/useAllSearchParam.md new file mode 100644 index 0000000000..ca6fa428c8 --- /dev/null +++ b/docs/useAllSearchParam.md @@ -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 ( +
+
params: {JSON.stringify(params)}
+
+ +
+
+ +
+
+ ); +}; +``` + +## 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 [``](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. diff --git a/src/index.ts b/src/index.ts index 62b69356b7..96698c5736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/useAllSearchParam.ts b/src/useAllSearchParam.ts new file mode 100644 index 0000000000..337d75b7cd --- /dev/null +++ b/src/useAllSearchParam.ts @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; +import { isBrowser, off, on } from './misc/util'; + +const getAllParams = (search: string): Record => { + const params = new URLSearchParams(search); + const result: Record = {}; + + params.forEach((value, key) => { + if (!(key in result)) { + result[key] = value; + } + }); + + return result; +}; + +export type UseAllSearchParam = () => Record; + +const useAllSearchParam: UseAllSearchParam = () => { + const location = window.location; + const [params, setParams] = useState>(() => 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; diff --git a/stories/useAllSearchParam.story.tsx b/stories/useAllSearchParam.story.tsx new file mode 100644 index 0000000000..4109f00461 --- /dev/null +++ b/stories/useAllSearchParam.story.tsx @@ -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 ( +
+
params: {JSON.stringify(params)}
+
+ +
+
+ +
+
+ ); +}; + +storiesOf('Sensors/useAllSearchParam', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/tests/useAllSearchParam.test.ts b/tests/useAllSearchParam.test.ts new file mode 100644 index 0000000000..880ab613a4 --- /dev/null +++ b/tests/useAllSearchParam.test.ts @@ -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' }); +});