10 | {JSON.stringify(queryState, null, ' ')}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/examples/use-location-state/src/index.tsx:
--------------------------------------------------------------------------------
1 | import '@ungap/url-search-params'
2 | import 'react-app-polyfill/ie11'
3 | import 'react-app-polyfill/stable'
4 | import React from 'react'
5 | import ReactDOM from 'react-dom'
6 | import './styles/index.scss'
7 | import App from './App'
8 |
9 | export function render() {
10 | ReactDOM.render(
10 | {JSON.stringify(queryState, null, ' ')}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/examples/react-router-use-location-state/src/index.tsx:
--------------------------------------------------------------------------------
1 | import '@ungap/url-search-params'
2 | import 'react-app-polyfill/ie11'
3 | import 'react-app-polyfill/stable'
4 | import React from 'react'
5 | import ReactDOM from 'react-dom'
6 | import './styles/index.scss'
7 | import App from './App'
8 |
9 | export function render() {
10 | ReactDOM.render(
7 | {
8 | "name": "Felix",
9 | "age": 25,
10 | "active": false
11 | }
12 |
13 | `;
14 |
15 | exports[`QueryStateDemo can set name with button 2`] = `
16 |
19 | {
20 | "name": "Kim",
21 | "age": 25,
22 | "active": false
23 | }
24 |
25 | `;
26 |
27 | exports[`QueryStateDemo can set name with button 3`] = `
28 |
31 | {
32 | "name": "Sarah",
33 | "age": 25,
34 | "active": false
35 | }
36 |
37 | `;
38 |
--------------------------------------------------------------------------------
/src/examples/nextjs/components/NamesList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useQueryState } from 'use-location-state/next'
3 |
4 | export default function NamesList() {
5 | const [names, setNames] = useQueryState('names', [])
6 |
7 | return (
8 |
7 | {
8 | "name": "Felix",
9 | "age": 25,
10 | "active": false,
11 | "date": "2019-01-01T00:00:00.000Z"
12 | }
13 |
14 | `;
15 |
16 | exports[`QueryStateDemo can set name with button 2`] = `
17 |
20 | {
21 | "name": "Kim",
22 | "age": 25,
23 | "active": false,
24 | "date": "2019-01-01T00:00:00.000Z"
25 | }
26 |
27 | `;
28 |
29 | exports[`QueryStateDemo can set name with button 3`] = `
30 |
33 | {
34 | "name": "Sarah",
35 | "age": 25,
36 | "active": false,
37 | "date": "2019-01-01T00:00:00.000Z"
38 | }
39 |
40 | `;
41 |
--------------------------------------------------------------------------------
/src/packages/use-location-state/src/useQueryState/useQueryState.types.ts:
--------------------------------------------------------------------------------
1 | import { QueryStateMerge } from 'query-state-core'
2 |
3 | export type QueryString = string
4 |
5 | export type SetQueryStateFn22 | 25 | 28 | 31 | {name !== 'Sarah' && ( 32 | 35 | )} 36 | 37 | setName(e.target.value)} 42 | /> 43 |
44 |46 | 49 | 52 | 55 | 56 | setAge(Number(e.target.value))} 61 | /> 62 |
63 |65 | 74 | 83 | 92 |
93 | 114 |14 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid 15 | architecto, atque, corporis debitis esse. 16 |
17 |26 | 29 | 32 | 35 | 36 | setName(e.target.value)} 41 | /> 42 |
43 |45 | 48 | 51 | 54 | 55 | setAge(Number(e.target.value))} 60 | /> 61 |
62 |64 | 73 | 82 | 91 |
92 | 113 |
7 | {
8 | "name": "Sarah",
9 | "age": 25,
10 | "active": true,
11 | "date": "2019-01-01T00:00:00.000Z"
12 | }
13 |
14 | `;
15 |
16 | exports[`LocationStateDemo can set active with checkbox 1`] = `
17 |
20 | {
21 | "name": "Sarah",
22 | "age": 25,
23 | "active": true,
24 | "date": "2019-01-01T00:00:00.000Z"
25 | }
26 |
27 | `;
28 |
29 | exports[`LocationStateDemo can set age with button 1`] = `
30 |
33 | {
34 | "name": "Sarah",
35 | "age": 30,
36 | "active": false,
37 | "date": "2019-01-01T00:00:00.000Z"
38 | }
39 |
40 | `;
41 |
42 | exports[`LocationStateDemo can set age with button 2`] = `
43 |
46 | {
47 | "name": "Sarah",
48 | "age": 45,
49 | "active": false,
50 | "date": "2019-01-01T00:00:00.000Z"
51 | }
52 |
53 | `;
54 |
55 | exports[`LocationStateDemo can set age with button 3`] = `
56 |
59 | {
60 | "name": "Sarah",
61 | "age": 25,
62 | "active": false,
63 | "date": "2019-01-01T00:00:00.000Z"
64 | }
65 |
66 | `;
67 |
68 | exports[`LocationStateDemo can set age with text field 1`] = `
69 |
72 | {
73 | "name": "Sarah",
74 | "age": 33,
75 | "active": false,
76 | "date": "2019-01-01T00:00:00.000Z"
77 | }
78 |
79 | `;
80 |
81 | exports[`LocationStateDemo can set date with date field 1`] = `
82 |
85 | {
86 | "name": "Sarah",
87 | "age": 25,
88 | "active": false,
89 | "date": "2019-05-01T00:00:00.000Z"
90 | }
91 |
92 | `;
93 |
94 | exports[`LocationStateDemo can set name & age with button 1`] = `
95 |
98 | {
99 | "name": "Felix",
100 | "age": 30,
101 | "active": false,
102 | "date": "2019-01-01T00:00:00.000Z"
103 | }
104 |
105 | `;
106 |
107 | exports[`LocationStateDemo can set name & age with button 2`] = `
108 |
111 | {
112 | "name": "Kim",
113 | "age": 45,
114 | "active": false,
115 | "date": "2019-01-01T00:00:00.000Z"
116 | }
117 |
118 | `;
119 |
120 | exports[`LocationStateDemo can set name & age with button 3`] = `
121 |
124 | {
125 | "name": "Sarah",
126 | "age": 25,
127 | "active": false,
128 | "date": "2019-01-01T00:00:00.000Z"
129 | }
130 |
131 | `;
132 |
133 | exports[`LocationStateDemo can set name with button 1`] = `
134 |
137 | {
138 | "name": "Felix",
139 | "age": 25,
140 | "active": false,
141 | "date": "2019-01-01T00:00:00.000Z"
142 | }
143 |
144 | `;
145 |
146 | exports[`LocationStateDemo can set name with button 2`] = `
147 |
150 | {
151 | "name": "Kim",
152 | "age": 25,
153 | "active": false,
154 | "date": "2019-01-01T00:00:00.000Z"
155 | }
156 |
157 | `;
158 |
159 | exports[`LocationStateDemo can set name with button 3`] = `
160 |
163 | {
164 | "name": "Sarah",
165 | "age": 25,
166 | "active": false,
167 | "date": "2019-01-01T00:00:00.000Z"
168 | }
169 |
170 | `;
171 |
172 | exports[`LocationStateDemo can set name with text field 1`] = `
173 |
176 | {
177 | "name": "Mila",
178 | "age": 25,
179 | "active": false,
180 | "date": "2019-01-01T00:00:00.000Z"
181 | }
182 |
183 | `;
184 |
185 | exports[`LocationStateDemo can set null in date field 1`] = `
186 |
189 | {
190 | "name": "Sarah",
191 | "age": 25,
192 | "active": false,
193 | "date": null
194 | }
195 |
196 | `;
197 |
198 | exports[`LocationStateDemo can set null in date field 2`] = `
199 |
202 | {
203 | "name": "Sarah",
204 | "age": 25,
205 | "active": false,
206 | "date": null
207 | }
208 |
209 | `;
210 |
--------------------------------------------------------------------------------
/src/examples/react-router-use-location-state/src/pages/test/__snapshots__/LocationStateTest.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`LocationStateDemo can set active with checkbox - push 1`] = `
4 |
7 | {
8 | "name": "Sarah",
9 | "age": 25,
10 | "active": false
11 | }
12 |
13 | `;
14 |
15 | exports[`LocationStateDemo can set active with checkbox 1`] = `
16 |
19 | {
20 | "name": "Sarah",
21 | "age": 25,
22 | "active": false
23 | }
24 |
25 | `;
26 |
27 | exports[`LocationStateDemo can set active with checkbox 2`] = `
28 |
31 | {
32 | "name": "Sarah",
33 | "age": 25,
34 | "active": true
35 | }
36 |
37 | `;
38 |
39 | exports[`LocationStateDemo can set age with button 1`] = `
40 |
43 | {
44 | "name": "Sarah",
45 | "age": 25,
46 | "active": false
47 | }
48 |
49 | `;
50 |
51 | exports[`LocationStateDemo can set age with button 2`] = `
52 |
55 | {
56 | "name": "Sarah",
57 | "age": 30,
58 | "active": false
59 | }
60 |
61 | `;
62 |
63 | exports[`LocationStateDemo can set age with button 3`] = `
64 |
67 | {
68 | "name": "Sarah",
69 | "age": 45,
70 | "active": false
71 | }
72 |
73 | `;
74 |
75 | exports[`LocationStateDemo can set age with button 4`] = `
76 |
79 | {
80 | "name": "Sarah",
81 | "age": 25,
82 | "active": false
83 | }
84 |
85 | `;
86 |
87 | exports[`LocationStateDemo can set age with text field 1`] = `
88 |
91 | {
92 | "name": "Sarah",
93 | "age": 25,
94 | "active": false
95 | }
96 |
97 | `;
98 |
99 | exports[`LocationStateDemo can set age with text field 2`] = `
100 |
103 | {
104 | "name": "Sarah",
105 | "age": 33,
106 | "active": false
107 | }
108 |
109 | `;
110 |
111 | exports[`LocationStateDemo can set name & age with button 1`] = `
112 |
115 | {
116 | "name": "Sarah",
117 | "age": 25,
118 | "active": false
119 | }
120 |
121 | `;
122 |
123 | exports[`LocationStateDemo can set name & age with button 2`] = `
124 |
127 | {
128 | "name": "Felix",
129 | "age": 30,
130 | "active": false
131 | }
132 |
133 | `;
134 |
135 | exports[`LocationStateDemo can set name & age with button 3`] = `
136 |
139 | {
140 | "name": "Kim",
141 | "age": 45,
142 | "active": false
143 | }
144 |
145 | `;
146 |
147 | exports[`LocationStateDemo can set name & age with button 4`] = `
148 |
151 | {
152 | "name": "Sarah",
153 | "age": 25,
154 | "active": false
155 | }
156 |
157 | `;
158 |
159 | exports[`LocationStateDemo can set name with button 1`] = `
160 |
163 | {
164 | "name": "Sarah",
165 | "age": 25,
166 | "active": false
167 | }
168 |
169 | `;
170 |
171 | exports[`LocationStateDemo can set name with button 2`] = `
172 |
175 | {
176 | "name": "Felix",
177 | "age": 25,
178 | "active": false
179 | }
180 |
181 | `;
182 |
183 | exports[`LocationStateDemo can set name with button 3`] = `
184 |
187 | {
188 | "name": "Kim",
189 | "age": 25,
190 | "active": false
191 | }
192 |
193 | `;
194 |
195 | exports[`LocationStateDemo can set name with button 4`] = `
196 |
199 | {
200 | "name": "Sarah",
201 | "age": 25,
202 | "active": false
203 | }
204 |
205 | `;
206 |
207 | exports[`LocationStateDemo can set name with text field 1`] = `
208 |
211 | {
212 | "name": "Sarah",
213 | "age": 25,
214 | "active": false
215 | }
216 |
217 | `;
218 |
219 | exports[`LocationStateDemo can set name with text field 2`] = `
220 |
223 | {
224 | "name": "Mila",
225 | "age": 25,
226 | "active": false
227 | }
228 |
229 | `;
230 |
--------------------------------------------------------------------------------
/src/packages/query-state-core/test/query-state-core.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createMergedQuery,
3 | EMPTY_ARRAY_STRING,
4 | parseQueryState,
5 | } from '../src/query-state-core'
6 |
7 | describe('parseQueryState parses', () => {
8 | it('empty string', () => {
9 | expect(parseQueryState('')).toEqual(null)
10 | })
11 |
12 | it('question mark string', () => {
13 | expect(parseQueryState('?')).toEqual(null)
14 | })
15 |
16 | it('query string with one parameter', () => {
17 | expect(parseQueryState('a=abc')).toEqual({ a: 'abc' })
18 | })
19 |
20 | it('query string with multiple parameters', () => {
21 | expect(parseQueryState('a=abc&d=efg')).toEqual({ a: 'abc', d: 'efg' })
22 | })
23 |
24 | it('query string with multiple parameters with the same name as array', () => {
25 | expect(parseQueryState('a=abc&a=efg')).toEqual({ a: ['abc', 'efg'] })
26 | })
27 |
28 | it('creates array from query string with multiple value values for same key', () => {
29 | expect(parseQueryState('filters=1&filters=2&filters=3')).toEqual({
30 | filters: ['1', '2', '3'],
31 | })
32 | })
33 |
34 | it('query string with "true" boolean parameter', () => {
35 | expect(parseQueryState('a=true')).toEqual({ a: 'true' })
36 | })
37 |
38 | it('query string with two boolean parameters', () => {
39 | expect(parseQueryState('b=false&c=true')).toEqual({ c: 'true', b: 'false' })
40 | })
41 |
42 | it('query string with parameter value containing spaces', () => {
43 | expect(parseQueryState('param=with%20space%20between')).toEqual({
44 | param: 'with space between',
45 | })
46 | })
47 |
48 | it('query string with invalid keys should be ignored', () => {
49 | const warnSpy = jest.spyOn(console, 'warn').mockImplementation()
50 | expect(parseQueryState('toString=true&valid=value&__proto__=true')).toEqual(
51 | { valid: 'value' }
52 | )
53 | expect(warnSpy).toHaveBeenCalledTimes(2)
54 | warnSpy.mockRestore()
55 | })
56 | })
57 |
58 | describe('createMergedQuery', () => {
59 | it('overwrites same keys with later value', () => {
60 | expect(createMergedQuery({ a: 'b' })).toEqual('a=b')
61 | })
62 |
63 | it('overwrites same keys with later value', () => {
64 | expect(createMergedQuery({ a: 'b' }, { a: 'c' })).toEqual('a=c')
65 | })
66 |
67 | it('stable sort keys', () => {
68 | const abcQueryString = 'a=true&b=true&c=true'
69 | expect(
70 | createMergedQuery({ a: 'true' }, { b: 'true' }, { c: 'true' })
71 | ).toEqual(abcQueryString)
72 | expect(
73 | createMergedQuery({ c: 'true' }, { a: 'true' }, { b: 'true' })
74 | ).toEqual(abcQueryString)
75 | expect(
76 | createMergedQuery({ b: 'true' }, { c: 'true' }, { a: 'true' })
77 | ).toEqual(abcQueryString)
78 | expect(createMergedQuery({ b: 'true', c: 'true', a: 'true' })).toEqual(
79 | abcQueryString
80 | )
81 | })
82 |
83 | it('with boolean values', () => {
84 | expect(createMergedQuery({ bool: 'true', bool2: 'false' })).toEqual(
85 | 'bool=true&bool2=false'
86 | )
87 | })
88 |
89 | it('with removed null values', () => {
90 | expect(createMergedQuery({ nullable: null })).toEqual('')
91 | })
92 |
93 | it('with removed undefined values', () => {
94 | expect(createMergedQuery({ undef: undefined })).toEqual('')
95 | })
96 |
97 | it('with removed entries by overwrite with null', () => {
98 | expect(createMergedQuery({ nullable: 'true' }, { nullable: null })).toEqual(
99 | ''
100 | )
101 | })
102 |
103 | it('with boolean overwriting null', () => {
104 | expect(createMergedQuery({ nulled: null }, { nulled: 'false' })).toEqual(
105 | 'nulled=false'
106 | )
107 | })
108 |
109 | it('allows empty objects', () => {
110 | expect(createMergedQuery({}, { a: 'c' }, {})).toEqual('a=c')
111 | })
112 |
113 | it('with removed entries by overwrite with undefined', () => {
114 | expect(
115 | createMergedQuery(
116 | { otherwiseEmpty: 'false', notDefined: 'true' },
117 | { notDefined: undefined },
118 | { otherwiseEmpty: 'true' }
119 | )
120 | ).toEqual('otherwiseEmpty=true')
121 | })
122 |
123 | it('with array value', () => {
124 | expect(createMergedQuery({ arr: ['1', '2', '3'] })).toEqual(
125 | 'arr=1&arr=2&arr=3'
126 | )
127 | })
128 |
129 | it('with array value overwrite (no automatic concat)', () => {
130 | expect(createMergedQuery({ arr: ['1'] }, { arr: ['a', 'b'] })).toEqual(
131 | 'arr=a&arr=b'
132 | )
133 | })
134 |
135 | it('with array with empty', () => {
136 | expect(createMergedQuery({ arr: [''] })).toEqual('arr=')
137 | })
138 |
139 | it('with empty array', () => {
140 | expect(createMergedQuery({ arr: [] })).toEqual(
141 | 'arr=' + encodeURIComponent(EMPTY_ARRAY_STRING)
142 | )
143 | })
144 | })
145 |
--------------------------------------------------------------------------------
/src/packages/use-location-state/src/useLocationState/useLocationReducer.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from 'react'
2 | import useLocationStateInterface from './useLocationStateInterface'
3 | import {
4 | LocationStateOpts,
5 | SetLocationStateOptions,
6 | } from './useLocationState.types'
7 | import { useRefLatest } from '../hooks/useRefLatest'
8 |
9 | const validTypes = ['string', 'number', 'boolean', 'object', 'undefined']
10 | const locationStateOptsDefaults: LocationStateOpts = Object.freeze({})
11 |
12 | export type LocationDispatch = (
13 | value: A,
14 | opts?: SetLocationStateOptions
15 | ) => void
16 | export type LocationReducerFnSome active content
} 113 |