state-in-url 2.2.0 → 2.2.1
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 +87 -82
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
|
|
|
20
20
|
|
|
21
21
|
<div align="center">
|
|
22
22
|
|
|
23
|
-
<h4 align="center">Don't hesitate to open an issue if you found a bug</h4>
|
|
23
|
+
<h4 align="center">Don't hesitate to open an issue if you found a bug, or for requesting features</h4>
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
@@ -50,10 +50,10 @@ Add a ⭐️ to support the project!
|
|
|
50
50
|
|
|
51
51
|
# Features
|
|
52
52
|
|
|
53
|
-
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
|
|
54
|
-
- 📘 **Typescript support and type Safety**: Preserves data types and structure,
|
|
53
|
+
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts, API similar to React.useState
|
|
54
|
+
- 📘 **Typescript support and type Safety**: Preserves data types and structure, good developer experience with IDE suggestions, strong typing and JSDoc comments
|
|
55
55
|
- ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
|
|
56
|
-
- ⚙ **Well tested**: Unit tests and Playwright tests
|
|
56
|
+
- ⚙ **Well tested**: Unit tests and Playwright tests, high quality and support
|
|
57
57
|
- ⚡ **Fast**: Minimal rerenders
|
|
58
58
|
- 🪶 **Lightweight**: Zero dependencies for a smaller footprint
|
|
59
59
|
|
|
@@ -88,7 +88,8 @@ pnpm add state-in-url
|
|
|
88
88
|
|
|
89
89
|
### 2. Edit tsconfig.json
|
|
90
90
|
|
|
91
|
-
In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "
|
|
91
|
+
In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Bundler"`, or`"moduleResolution": "Node16"`, or `"moduleResolution": "NodeNext"`.
|
|
92
|
+
Possibly need to set `"module": "ES2022"`, or `"module": "ESNext"`
|
|
92
93
|
|
|
93
94
|
## useUrlState hook for Next.js
|
|
94
95
|
|
|
@@ -103,11 +104,11 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
|
|
|
103
104
|
1. Define state shape
|
|
104
105
|
|
|
105
106
|
```typescript
|
|
106
|
-
//
|
|
107
|
+
// userState.ts
|
|
107
108
|
// State shape should be stored in a constant, don't pass an object directly
|
|
108
|
-
export const
|
|
109
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
109
110
|
|
|
110
|
-
type
|
|
111
|
+
type UserState = { name: string, age: number }
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
2. Import it and use
|
|
@@ -116,39 +117,37 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
|
|
|
116
117
|
'use client'
|
|
117
118
|
import { useUrlState } from 'state-in-url/next';
|
|
118
119
|
|
|
119
|
-
import {
|
|
120
|
+
import { userState } from './userState';
|
|
120
121
|
|
|
121
122
|
function MyComponent() {
|
|
122
|
-
// for use searchParams from server component
|
|
123
|
-
// e.g. export default async function Home({ searchParams }: { searchParams: object }) {
|
|
124
|
-
// const { state, updateState, updateUrl } = useUrlState({ defaultState: countState, searchParams });
|
|
125
123
|
// can pass `replace` arg, it's control will `updateUrl` will use `rounter.push` or `router.replace`, default replace=true
|
|
126
|
-
// const { state, updateState, updateUrl } = useUrlState({ defaultState:
|
|
127
|
-
const { state, updateState, updateUrl } = useUrlState({ defaultState:
|
|
124
|
+
// const { state, updateState, updateUrl } = useUrlState({ defaultState: userState, searchParams, replace: false });
|
|
125
|
+
const { state, updateState, updateUrl } = useUrlState({ defaultState: userState });
|
|
128
126
|
|
|
129
127
|
// won't let you to accidently mutate state directly, requires TS
|
|
130
|
-
// state.
|
|
128
|
+
// state.name = 'John' // <- error
|
|
131
129
|
|
|
132
130
|
return (
|
|
133
131
|
<div>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<button onClick={() => updateUrl(
|
|
132
|
+
<input value={state.name}
|
|
133
|
+
onChange={(ev) => { updateState({ name: ev.target.value }) }}
|
|
134
|
+
onBlur={() => updateUrl()}
|
|
135
|
+
/>
|
|
136
|
+
<input value={state.age}
|
|
137
|
+
onChange={(ev) => { updateState({ age: +ev.target.value }) }}
|
|
138
|
+
onBlur={() => updateUrl()}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
// same api as React.useState
|
|
142
|
+
<input value={state.name}
|
|
143
|
+
onChange={(ev) => { updateState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
144
|
+
onBlur={() => updateUrl()}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<button onClick={() => updateUrl(userState)}>
|
|
150
148
|
Reset
|
|
151
149
|
</button>
|
|
150
|
+
|
|
152
151
|
</div>
|
|
153
152
|
)
|
|
154
153
|
}
|
|
@@ -157,74 +156,80 @@ function MyComponent() {
|
|
|
157
156
|
#### With complex state shape
|
|
158
157
|
|
|
159
158
|
```typescript
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
export const form: Form = {
|
|
160
|
+
name: '',
|
|
161
|
+
age: undefined,
|
|
162
|
+
'agree to terms': false,
|
|
163
|
+
tags: [],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type Form = {
|
|
167
|
+
name: string;
|
|
168
|
+
age?: number;
|
|
169
|
+
'agree to terms': boolean;
|
|
170
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
171
|
+
};
|
|
165
172
|
|
|
166
|
-
export const userSettings: UserSettings {
|
|
167
|
-
theme: 'light',
|
|
168
|
-
fontSize: 16,
|
|
169
|
-
notifications: true,
|
|
170
|
-
}
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
```typescript
|
|
174
176
|
'use client'
|
|
175
177
|
import { useUrlState } from 'state-in-url/next';
|
|
176
178
|
|
|
177
|
-
import {
|
|
178
|
-
|
|
179
|
-
function SettingsComponent() {
|
|
180
|
-
// `state` will infer from UserSettings type!
|
|
181
|
-
const { state, updateUrl } = useUrlState({ defaultState: userSettings });
|
|
182
|
-
|
|
183
|
-
const toggleTheme = () => {
|
|
184
|
-
updateUrl(current => ({
|
|
185
|
-
...current,
|
|
186
|
-
theme: current.theme === 'light' ? 'dark' : 'light',
|
|
187
|
-
}));
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// sync state to url when idle
|
|
191
|
-
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
192
|
-
React.useEffect(() => {
|
|
193
|
-
clearTimeout(timer.current);
|
|
194
|
-
timer.current = setTimeout(() => {
|
|
195
|
-
// will compare state by content not by reference and fire update only for new values
|
|
196
|
-
updateUrl(state);
|
|
197
|
-
}, 500);
|
|
179
|
+
import { form } from './form';
|
|
198
180
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
181
|
+
function TagsComponent() {
|
|
182
|
+
// `state` will infer from Form type!
|
|
183
|
+
const { state, updateUrl } = useUrlState({ defaultState: form });
|
|
184
|
+
|
|
185
|
+
const onChangeTags = React.useCallback(
|
|
186
|
+
(tag: (typeof tags)[number]) => {
|
|
187
|
+
updateUrl((curr) => ({
|
|
188
|
+
...curr,
|
|
189
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
190
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
191
|
+
: curr.tags.concat(tag),
|
|
192
|
+
}));
|
|
193
|
+
},
|
|
194
|
+
[updateUrl],
|
|
195
|
+
);
|
|
203
196
|
|
|
204
197
|
return (
|
|
205
198
|
<div>
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
<Field text="Tags">
|
|
200
|
+
<div className="flex flex-wrap gap-2">
|
|
201
|
+
{tags.map((tag) => (
|
|
202
|
+
<Tag
|
|
203
|
+
active={!!state.tags.find((t) => t.id === tag.id)}
|
|
204
|
+
text={tag.value.text}
|
|
205
|
+
onClick={() => onChangeTags(tag)}
|
|
206
|
+
key={tag.id}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</Field>
|
|
211
211
|
</div>
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
|
-
...
|
|
215
|
-
|
|
216
|
-
// Other component
|
|
217
|
-
function Component() {
|
|
218
|
-
const { state } = useUrlState({ defaultState: defaultSettings });
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
215
|
+
const tags = [
|
|
216
|
+
{
|
|
217
|
+
id: '1',
|
|
218
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: '2',
|
|
222
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: '3',
|
|
226
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
227
|
+
},
|
|
228
|
+
];
|
|
226
229
|
```
|
|
227
230
|
|
|
231
|
+
[Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/main/packages/example-nextjs/src/app/Form.tsx)
|
|
232
|
+
|
|
228
233
|
#### Auto sync state
|
|
229
234
|
|
|
230
235
|
```typescript
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"react";import{parseSsrQs as e}from"../next/parseSsrQs.mjs";import{useInsertionEffect as n}from"../useInsertionEffect.mjs";import{useSharedState as o}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as r}from"../useUrlEncode/useUrlEncode.mjs";import{isSSR as s}from"../utils.mjs";function a(a,m,
|
|
1
|
+
import t from"react";import{parseSsrQs as e}from"../next/parseSsrQs.mjs";import{useInsertionEffect as n}from"../useInsertionEffect.mjs";import{useSharedState as o}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as r}from"../useUrlEncode/useUrlEncode.mjs";import{isSSR as s}from"../utils.mjs";function a(a,m,l){const{parse:u,stringify:w}=r(a),{state:f,getState:p,setState:d}=o(a,(()=>s()?e(function(t,e){const n=Object.keys(t),o=Object.fromEntries(Object.entries(e||{}).map((([t,e])=>[t.replaceAll("+"," "),e])).filter((([t])=>n.includes(t))));return o}(a,l),a):u(c(a))));n((()=>{const t=()=>{const t=u(c(a));d(t)};return window.addEventListener(i,t),()=>{window.removeEventListener(i,t)}}),[d]);const h=t.useCallback(((t,e)=>{const n=`${window.location.pathname}${window.location.search}${window.location.hash}`,o="function"==typeof t,r=function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(a),s=o?t(p()):t?{...p(),...t}:p(),c=w(s,r);d(s);const i=`${window.location.pathname}${c.length?"?":""}${c}${window.location.hash}`;if(n!==i){const{replace:t,...n}=e||{};m[t?"replace":"push"](i,{...n})}}),[m,w,p]);return{updateState:d,updateUrl:h,state:f,getState:p}}function c(t){const e=new URLSearchParams(window.location.search),n=Object.keys(t),o=new URLSearchParams;return[...e.entries()].filter((([t])=>n.includes(t))).forEach((([t,e])=>o.set(t,e))),o.toString()}const i="popstate";export{a as useUrlStateBase};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers.",
|
|
5
5
|
"homepage": "https://github.com/asmyshlyaev177/state-in-url#readme",
|
|
6
6
|
"keywords": [
|