Skip to content

Commit 72935e2

Browse files
authored
feat(hooks): add use-query-params (#74)
1 parent dc04b39 commit 72935e2

File tree

9 files changed

+699
-10
lines changed

9 files changed

+699
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ scaleway-lib is a set of NPM packages used at Scaleway.
2727
- [`@scaleway/countries`](./packages/countries/README.md): ISO 3166/3166-2 coutries JSON database
2828
- [`@scaleway/eslint-config-react`](./packages/eslint-config-react/README.md): A shared eslint react opiniated configuration
2929
- [`@scaleway/random-name`](./packages/random-name/README.md): A tiny utility to generate random names
30+
- [`@scaleway/use-query-params`](./packages/use-query-params/README.md): A tiny react hook to read and update URLs query parameters
3031
- [`@scaleway/regex`](./packages/regex/README.md): usefull regex named
3132

3233
## Development

babel.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"presets": ["@babel/preset-env"],
2+
"presets": ["@babel/preset-env","@babel/preset-react"],
33
"plugins": ["@babel/plugin-transform-runtime"]
44
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@babel/core": "^7.12.10",
99
"@babel/plugin-transform-runtime": "^7.12.10",
1010
"@babel/preset-env": "^7.12.11",
11+
"@babel/preset-react":"7.12.13",
1112
"@commitlint/cli": "^12.0.0",
1213
"@commitlint/config-conventional": "^12.0.0",
1314
"@rollup/plugin-babel": "^5.2.2",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/__tests__/**
2+
src
3+
!.npmignore
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# `@scaleway/use-query-params`
2+
3+
## A tiny hooks to handle use-query-params
4+
5+
## Install
6+
7+
```bash
8+
$ yarn add @scaleway/use-query-params
9+
```
10+
11+
## Usage
12+
13+
```js
14+
import React from 'react'
15+
import useQueryParams from '@scaleway/use-query-params'
16+
17+
// this component should be wrap with a Router
18+
const Component = () => {
19+
const { queryParams, setQueryParams } = useQueryParams()
20+
const { user } = queryParams
21+
const setUser = () => setQueryParams({ user: 'John' })
22+
// ?user=John
23+
24+
return (
25+
<>
26+
<h1>User: {user}</h1>
27+
<button onClick={setUser}>Set User John</button>
28+
</>
29+
)
30+
}
31+
```
32+
33+
## Usage with a custom updater
34+
35+
```js
36+
import React from 'react'
37+
import useQueryParams from '@scaleway/use-query-params'
38+
39+
// this component should be wrap with a Router
40+
const Component = () => {
41+
const updater = (prevState, nextState) => ({
42+
...prevState,
43+
...Object.keys(nextState).reduce(
44+
(acc, key) => ({ ...acc, [key]: nextState[key].toUpperCase() }),
45+
{},
46+
),
47+
})
48+
const { queryParams, setQueryParams } = useQueryParams({ updater })
49+
const { user } = queryParams
50+
const setUser = () => setQueryParams({ user: 'John' })
51+
// ?user=JOHN
52+
53+
return (
54+
<>
55+
<h1>User: {user}</h1>
56+
<button onClick={setUser}>Set User John</button>
57+
</>
58+
)
59+
}
60+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@scaleway/use-query-params",
3+
"version": "1.0.0",
4+
"description": "A small hook to handle params",
5+
"keywords": [
6+
"react",
7+
"react-dom",
8+
"reactjs",
9+
"hooks",
10+
"react-router-dom",
11+
"params",
12+
"query-params"
13+
],
14+
"main": "dist/index.js",
15+
"module": "dist/module.js",
16+
"browser": {
17+
"dist/index.js": "./dist/index.browser.js",
18+
"dist/module.js": "./dist/module.browser.js"
19+
},
20+
"publishConfig": {
21+
"access": "public"
22+
},
23+
"repository": {
24+
"type": "git",
25+
"url": "https://github.com/scaleway/scaleway-lib",
26+
"directory": "packages/use-query-params"
27+
},
28+
"license": "MIT",
29+
"dependencies": {
30+
"query-string": "^6.14.1"
31+
},
32+
"peerDependencies": {
33+
"react": "17.x",
34+
"react-dom": "17.x",
35+
"react-router-dom": "5.x"
36+
},
37+
"devDependencies": {
38+
"@testing-library/jest-dom": "^5.11.9",
39+
"@testing-library/react": "^11.2.5",
40+
"@testing-library/react-hooks": "^5.1.0",
41+
"react": "^17.0.1",
42+
"react-dom": "^17.0.1",
43+
"react-router-dom": "^5.2.0"
44+
}
45+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { act, cleanup, renderHook } from '@testing-library/react-hooks'
2+
import React from 'react'
3+
import { MemoryRouter } from 'react-router-dom'
4+
import useQueryParam from '../index'
5+
6+
// eslint-disable-next-line react/prop-types
7+
const wrapper = ({ pathname = 'one', search }) => ({ children }) => (
8+
<MemoryRouter initialIndex={0} initialEntries={[{ pathname, search }]}>
9+
{children}
10+
</MemoryRouter>
11+
)
12+
13+
describe('useQueryParam', () => {
14+
afterEach(cleanup)
15+
it('should set one object', async () => {
16+
const { result } = renderHook(() => useQueryParam(), {
17+
wrapper: MemoryRouter,
18+
})
19+
20+
act(() => {
21+
result.current.setQueryParams({ user: 'John' })
22+
})
23+
expect(result.current.queryParams).toEqual({ user: 'John' })
24+
})
25+
26+
it('should correctly set with different value', async () => {
27+
const { result } = renderHook(() => useQueryParam(), {
28+
wrapper: MemoryRouter,
29+
})
30+
31+
act(() => {
32+
result.current.setQueryParams({ user: 'John' })
33+
})
34+
expect(result.current.queryParams).toEqual({ user: 'John' })
35+
36+
act(() => {
37+
result.current.setQueryParams({ user: 'Doe' })
38+
})
39+
expect(result.current.queryParams).toEqual({ user: 'Doe' })
40+
41+
act(() => {
42+
result.current.setQueryParams({ user: 'Scaleway' })
43+
})
44+
expect(result.current.queryParams).toEqual({ user: 'Scaleway' })
45+
})
46+
47+
it('should set one complexe object', async () => {
48+
const { result } = renderHook(() => useQueryParam(), {
49+
wrapper: MemoryRouter,
50+
})
51+
52+
act(() => {
53+
result.current.setQueryParams({
54+
user: 'John Doe',
55+
name: 'John',
56+
lastName: 'Doe',
57+
version: 1234,
58+
lib: 'useQueryParams',
59+
})
60+
})
61+
expect(result.current.queryParams).toEqual({
62+
user: 'John Doe',
63+
name: 'John',
64+
lastName: 'Doe',
65+
version: 1234,
66+
lib: 'useQueryParams',
67+
})
68+
})
69+
70+
it('should get queryParams from existing location', () => {
71+
const { result } = renderHook(() => useQueryParam(), {
72+
wrapper: wrapper({ search: 'user=john' }),
73+
})
74+
75+
expect(result.current.queryParams).toEqual({ user: 'john' })
76+
})
77+
it('should should handle array, boolean, number and string from existing location', () => {
78+
const { result } = renderHook(() => useQueryParam(), {
79+
wrapper: wrapper({
80+
search: 'string=john&boolean=true&number=123&array=handle,array,format',
81+
}),
82+
})
83+
84+
expect(result.current.queryParams).toEqual({
85+
string: 'john',
86+
boolean: true,
87+
number: 123,
88+
array: ['handle', 'array', 'format'],
89+
})
90+
})
91+
92+
it('should get queryParams from existing location and set new params', () => {
93+
const { result } = renderHook(() => useQueryParam(), {
94+
wrapper: wrapper({ search: 'user=john' }),
95+
})
96+
97+
expect(result.current.queryParams).toEqual({ user: 'john' })
98+
99+
act(() => {
100+
result.current.setQueryParams({
101+
lastName: 'Doe',
102+
version: 1234,
103+
lib: 'useQueryParams',
104+
})
105+
})
106+
107+
expect(result.current.queryParams).toEqual({
108+
user: 'john',
109+
lastName: 'Doe',
110+
version: 1234,
111+
lib: 'useQueryParams',
112+
})
113+
})
114+
115+
it('should modify updater and erase old params', () => {
116+
const updater = (prevState, nextState) => nextState
117+
const { result } = renderHook(() => useQueryParam({ updater }), {
118+
wrapper: wrapper({ search: 'user=john' }),
119+
})
120+
121+
expect(result.current.queryParams).toEqual({ user: 'john' })
122+
act(() => {
123+
result.current.setQueryParams({
124+
lastName: 'Doe',
125+
})
126+
})
127+
expect(result.current.queryParams).toEqual({
128+
lastName: 'Doe',
129+
})
130+
})
131+
132+
it('should modify updater and uppercase all news params', () => {
133+
const updater = (prevState, nextState) => ({
134+
...prevState,
135+
...Object.keys(nextState).reduce(
136+
(acc, key) => ({ ...acc, [key]: nextState[key].toUpperCase() }),
137+
{},
138+
),
139+
})
140+
141+
const { result } = renderHook(() => useQueryParam({ updater }), {
142+
wrapper: wrapper({ search: 'user=john' }),
143+
})
144+
145+
expect(result.current.queryParams).toEqual({ user: 'john' })
146+
147+
act(() => {
148+
result.current.setQueryParams({
149+
lastName: 'Doe',
150+
})
151+
})
152+
153+
expect(result.current.queryParams).toEqual({
154+
user: 'john',
155+
lastName: 'DOE',
156+
})
157+
})
158+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { parse, stringify } from 'query-string'
2+
import { useCallback, useEffect, useState } from 'react'
3+
import { useHistory } from 'react-router-dom'
4+
5+
const useQueryParams = (options = {}) => {
6+
const { updater = {} } = options
7+
const {
8+
location: { search, pathname },
9+
replace,
10+
} = useHistory()
11+
12+
const parseFormat = useCallback(
13+
() =>
14+
parse(search, {
15+
parseNumbers: true,
16+
parseBooleans: true,
17+
arrayFormat: 'comma',
18+
}),
19+
[search],
20+
)
21+
22+
const stringyFormat = useCallback(
23+
params =>
24+
stringify(params, {
25+
arrayFormat: 'comma',
26+
sort: (a, b) => a.localeCompare(b),
27+
}),
28+
[],
29+
)
30+
31+
const defaultFnUpdater = useCallback(
32+
(currentQueryParams, nextQueryParams) => ({
33+
...currentQueryParams,
34+
...nextQueryParams,
35+
}),
36+
[],
37+
)
38+
const [state, setState] = useState(parseFormat())
39+
40+
const setQueryParams = useCallback(
41+
nextParams => {
42+
const currentQueryParams = parseFormat()
43+
const params =
44+
updater instanceof Function
45+
? updater(currentQueryParams, nextParams)
46+
: defaultFnUpdater(currentQueryParams, nextParams)
47+
48+
setState(params)
49+
},
50+
[parseFormat, updater, defaultFnUpdater],
51+
)
52+
53+
useEffect(() => {
54+
const stringifiedParams = stringyFormat(state)
55+
if (stringifiedParams !== search.replace('?', '')) {
56+
replace(`${pathname}?${stringifiedParams}`)
57+
}
58+
}, [pathname, replace, search, state, stringyFormat])
59+
60+
61+
return {
62+
queryParams: state,
63+
setQueryParams,
64+
}
65+
}
66+
67+
export default useQueryParams

0 commit comments

Comments
 (0)