zustand-querystring 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -60
- package/dist/chunk-TQQUWVLF.mjs +367 -0
- package/dist/format/json.d.mts +6 -0
- package/dist/format/json.d.ts +6 -0
- package/dist/format/json.js +59 -0
- package/dist/format/json.mjs +35 -0
- package/dist/format/marked.d.mts +67 -0
- package/dist/format/marked.d.ts +67 -0
- package/dist/format/marked.js +393 -0
- package/dist/format/marked.mjs +14 -0
- package/dist/format/plain.d.mts +49 -0
- package/dist/format/plain.d.ts +49 -0
- package/dist/format/plain.js +446 -0
- package/dist/format/plain.mjs +421 -0
- package/dist/index.d.mts +33 -16
- package/dist/index.d.ts +33 -16
- package/dist/index.js +442 -105
- package/dist/index.mjs +89 -106
- package/package.json +34 -8
- package/dist/format/readable.d.mts +0 -14
- package/dist/format/readable.d.ts +0 -14
- package/dist/format/readable.js +0 -238
- package/dist/format/readable.mjs +0 -212
package/README.md
CHANGED
|
@@ -1,84 +1,301 @@
|
|
|
1
1
|
# zustand-querystring
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zustand middleware for URL query string sync.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npm install zustand-querystring
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { create } from 'zustand';
|
|
13
|
+
import { querystring } from 'zustand-querystring';
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
const useStore = create(
|
|
16
|
+
querystring(
|
|
17
|
+
(set) => ({
|
|
18
|
+
search: '',
|
|
19
|
+
page: 1,
|
|
20
|
+
setSearch: (search) => set({ search }),
|
|
21
|
+
setPage: (page) => set({ page }),
|
|
22
|
+
}),
|
|
23
|
+
{
|
|
24
|
+
select: () => ({ search: true, page: true }),
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
// URL: ?search=hello&page=2
|
|
29
|
+
```
|
|
8
30
|
|
|
9
|
-
|
|
10
|
-
- [NextJS](./examples/next/)
|
|
31
|
+
---
|
|
11
32
|
|
|
12
|
-
|
|
33
|
+
## Options
|
|
13
34
|
|
|
14
35
|
```ts
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
querystring(storeCreator, {
|
|
37
|
+
select: undefined, // which fields to sync
|
|
38
|
+
key: false, // false | 'state'
|
|
39
|
+
prefix: '', // prefix for URL params
|
|
40
|
+
format: marked, // serialization format
|
|
41
|
+
syncNull: false, // sync null values
|
|
42
|
+
syncUndefined: false, // sync undefined values
|
|
43
|
+
url: undefined, // request URL for SSR
|
|
44
|
+
})
|
|
45
|
+
```
|
|
17
46
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
### `select`
|
|
48
|
+
|
|
49
|
+
Controls which state fields sync to URL. Receives pathname, returns object with `true` for fields to sync.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// All fields
|
|
53
|
+
select: () => ({ search: true, page: true, filters: true })
|
|
54
|
+
|
|
55
|
+
// Route-based
|
|
56
|
+
select: (pathname) => ({
|
|
57
|
+
search: true,
|
|
58
|
+
filters: pathname.startsWith('/products'),
|
|
59
|
+
adminSettings: pathname.startsWith('/admin'),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Nested fields
|
|
63
|
+
select: () => ({
|
|
64
|
+
user: {
|
|
65
|
+
name: true,
|
|
66
|
+
settings: { theme: true },
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `key`
|
|
72
|
+
|
|
73
|
+
- `false` (default): Each field becomes a separate URL param
|
|
74
|
+
```
|
|
75
|
+
?search=hello&page=2&filters.sort=name
|
|
76
|
+
```
|
|
77
|
+
- `'state'` (or any string): All state in one param
|
|
78
|
+
```
|
|
79
|
+
?state=search%3Dhello%2Cpage%3A2
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `prefix`
|
|
83
|
+
|
|
84
|
+
Adds prefix to all params. Use when multiple stores share URL.
|
|
26
85
|
|
|
27
|
-
|
|
86
|
+
```ts
|
|
87
|
+
querystring(storeA, { prefix: 'a_', select: () => ({ search: true }) })
|
|
88
|
+
querystring(storeB, { prefix: 'b_', select: () => ({ filter: true }) })
|
|
89
|
+
// URL: ?a_search=hello&b_filter=active
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `syncNull` / `syncUndefined`
|
|
93
|
+
|
|
94
|
+
By default, `null` and `undefined` reset to initial state (removed from URL). Set to `true` to write them.
|
|
95
|
+
|
|
96
|
+
### `url`
|
|
97
|
+
|
|
98
|
+
For SSR, pass the request URL:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
querystring(store, { url: request.url, select: () => ({ search: true }) })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## How State Syncs
|
|
107
|
+
|
|
108
|
+
1. **On page load**: URL → State
|
|
109
|
+
2. **On state change**: State → URL (via `replaceState`)
|
|
110
|
+
|
|
111
|
+
Only values **different from initial state** are written to URL:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Initial: { search: '', page: 1, sort: 'date' }
|
|
115
|
+
// Current: { search: 'hello', page: 1, sort: 'name' }
|
|
116
|
+
// URL: ?search=hello&sort=name
|
|
117
|
+
// (page omitted - matches initial)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Type handling:
|
|
121
|
+
- Objects: recursively diffed
|
|
122
|
+
- Arrays, Dates: compared as whole values
|
|
123
|
+
- Functions: never synced
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Formats
|
|
128
|
+
|
|
129
|
+
Three built-in formats:
|
|
130
|
+
|
|
131
|
+
| Format | Example Output |
|
|
132
|
+
|--------|----------------|
|
|
133
|
+
| `marked` | `count:5,tags@a,b~` |
|
|
134
|
+
| `plain` | `count=5&tags=a,b` |
|
|
135
|
+
| `json` | `count=5&tags=%5B%22a%22%5D` |
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { marked } from 'zustand-querystring/format/marked';
|
|
139
|
+
import { plain } from 'zustand-querystring/format/plain';
|
|
140
|
+
import { json } from 'zustand-querystring/format/json';
|
|
141
|
+
|
|
142
|
+
querystring(store, { format: plain })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Marked Format (default)
|
|
146
|
+
|
|
147
|
+
Type markers: `:` primitive, `=` string, `@` array, `.` object
|
|
148
|
+
|
|
149
|
+
Delimiters: `,` separator, `~` terminator, `_` escape
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { createFormat } from 'zustand-querystring/format/marked';
|
|
153
|
+
|
|
154
|
+
const format = createFormat({
|
|
155
|
+
typeObject: '.',
|
|
156
|
+
typeArray: '@',
|
|
157
|
+
typeString: '=',
|
|
158
|
+
typePrimitive: ':',
|
|
159
|
+
separator: ',',
|
|
160
|
+
terminator: '~',
|
|
161
|
+
escapeChar: '_',
|
|
162
|
+
datePrefix: 'D',
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Plain Format
|
|
167
|
+
|
|
168
|
+
Dot notation for nesting, comma-separated arrays.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { createFormat } from 'zustand-querystring/format/plain';
|
|
172
|
+
|
|
173
|
+
const format = createFormat({
|
|
174
|
+
entrySeparator: ',', // between entries in namespaced mode
|
|
175
|
+
nestingSeparator: '.', // for nested keys
|
|
176
|
+
arraySeparator: ',', // or 'repeat' for ?tags=a&tags=b&tags=c
|
|
177
|
+
escapeChar: '_',
|
|
178
|
+
nullString: 'null',
|
|
179
|
+
undefinedString: 'undefined',
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### JSON Format
|
|
184
|
+
|
|
185
|
+
URL-encoded JSON. No configuration.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Custom Format
|
|
190
|
+
|
|
191
|
+
Implement `QueryStringFormat`:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import type { QueryStringFormat, QueryStringParams, ParseContext } from 'zustand-querystring';
|
|
195
|
+
|
|
196
|
+
const myFormat: QueryStringFormat = {
|
|
197
|
+
// For key: 'state' (namespaced mode)
|
|
198
|
+
stringify(state: object): string {
|
|
199
|
+
return encodeURIComponent(JSON.stringify(state));
|
|
200
|
+
},
|
|
201
|
+
parse(value: string, ctx?: ParseContext): object {
|
|
202
|
+
return JSON.parse(decodeURIComponent(value));
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// For key: false (standalone mode)
|
|
206
|
+
stringifyStandalone(state: object): QueryStringParams {
|
|
207
|
+
const result: QueryStringParams = {};
|
|
208
|
+
for (const [key, value] of Object.entries(state)) {
|
|
209
|
+
result[key] = [encodeURIComponent(JSON.stringify(value))];
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
},
|
|
213
|
+
parseStandalone(params: QueryStringParams, ctx: ParseContext): object {
|
|
214
|
+
const result: Record<string, unknown> = {};
|
|
215
|
+
for (const [key, values] of Object.entries(params)) {
|
|
216
|
+
result[key] = JSON.parse(decodeURIComponent(values[0]));
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
querystring(store, { format: myFormat })
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Types:
|
|
226
|
+
- `QueryStringParams` = `Record<string, string[]>` (values always arrays)
|
|
227
|
+
- `ctx.initialState` available for type coercion
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Examples
|
|
232
|
+
|
|
233
|
+
### Search with reset
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const useStore = create(
|
|
28
237
|
querystring(
|
|
29
|
-
(set
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
hello: 'Hello',
|
|
35
|
-
},
|
|
238
|
+
(set) => ({
|
|
239
|
+
query: '',
|
|
240
|
+
page: 1,
|
|
241
|
+
setQuery: (query) => set({ query, page: 1 }), // reset page on new query
|
|
242
|
+
setPage: (page) => set({ page }),
|
|
36
243
|
}),
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
// pathname is the current route (e.g. /about or /)
|
|
40
|
-
select(pathname) {
|
|
41
|
-
return {
|
|
42
|
-
count: true,
|
|
43
|
-
// ticks: false, <- false by default
|
|
44
|
-
|
|
45
|
-
someNestedState: {
|
|
46
|
-
nestedCount: true,
|
|
47
|
-
hello: '/about' === pathname,
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
// OR select the whole nested state
|
|
51
|
-
// someNestedState: true
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
),
|
|
244
|
+
{ select: () => ({ query: true, page: true }) }
|
|
245
|
+
)
|
|
56
246
|
);
|
|
57
247
|
```
|
|
58
248
|
|
|
59
|
-
|
|
249
|
+
### Multiple stores with prefixes
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const useFilters = create(
|
|
253
|
+
querystring(filtersStore, {
|
|
254
|
+
prefix: 'f_',
|
|
255
|
+
select: () => ({ category: true, price: true }),
|
|
256
|
+
})
|
|
257
|
+
);
|
|
60
258
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
259
|
+
const usePagination = create(
|
|
260
|
+
querystring(paginationStore, {
|
|
261
|
+
prefix: 'p_',
|
|
262
|
+
select: () => ({ page: true, limit: true }),
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
// URL: ?f_category=shoes&f_price=100&p_page=2&p_limit=20
|
|
266
|
+
```
|
|
67
267
|
|
|
68
|
-
|
|
268
|
+
### Next.js SSR
|
|
69
269
|
|
|
70
|
-
|
|
270
|
+
```ts
|
|
271
|
+
// app/page.tsx
|
|
272
|
+
export default async function Page({ searchParams }) {
|
|
273
|
+
// Store reads from URL on init
|
|
274
|
+
}
|
|
275
|
+
```
|
|
71
276
|
|
|
72
|
-
|
|
277
|
+
---
|
|
73
278
|
|
|
74
|
-
|
|
279
|
+
## Exports
|
|
75
280
|
|
|
76
|
-
|
|
281
|
+
```ts
|
|
282
|
+
// Middleware
|
|
283
|
+
import { querystring } from 'zustand-querystring';
|
|
77
284
|
|
|
78
|
-
|
|
285
|
+
// Formats
|
|
286
|
+
import { marked, createFormat } from 'zustand-querystring/format/marked';
|
|
287
|
+
import { plain, createFormat } from 'zustand-querystring/format/plain';
|
|
288
|
+
import { json } from 'zustand-querystring/format/json';
|
|
289
|
+
|
|
290
|
+
// Types
|
|
291
|
+
import type {
|
|
292
|
+
QueryStringOptions,
|
|
293
|
+
QueryStringFormat,
|
|
294
|
+
QueryStringParams,
|
|
295
|
+
ParseContext,
|
|
296
|
+
} from 'zustand-querystring';
|
|
297
|
+
```
|
|
79
298
|
|
|
80
|
-
|
|
299
|
+
---
|
|
81
300
|
|
|
82
|
-
-
|
|
83
|
-
- **Arrays, Dates, RegExp, Maps, Sets, and class instances** are compared as atomic values - if any part changes, the entire value is synced
|
|
84
|
-
- **Functions** are never synced to the URL
|
|
301
|
+
[Playground](https://stackblitz.com/github/nitedani/zustand-querystring/tree/main/examples/react) · [GitHub](https://github.com/nitedani/zustand-querystring)
|