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 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
  ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
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, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
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": "Node16"` or `"moduleResolution": "NodeNext"`
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
- // countState.ts
107
+ // userState.ts
107
108
  // State shape should be stored in a constant, don't pass an object directly
108
- export const countState: CountState = { count: 0 }
109
+ export const userState: UserState = { name: '', age: 0 }
109
110
 
110
- type CountState = { count: number }
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 { countState } from './countState';
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: countState, searchParams, replace: false });
127
- const { state, updateState, updateUrl } = useUrlState({ defaultState: countState });
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.count = 2 // <- error
128
+ // state.name = 'John' // <- error
131
129
 
132
130
  return (
133
131
  <div>
134
- <p>Count: {state.count}</p>
135
-
136
- <button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
137
- Increment (Update URL)
138
- </button>
139
-
140
- // same api as React.useState
141
- <button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
142
- Increment (Local Only)
143
- </button>
144
- <button onClick={() => updateUrl()}>
145
- Sync changes to url
146
- // Or don't sync it and just share state
147
- </button>
148
-
149
- <button onClick={() => updateUrl(state)}>
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
- interface UserSettings {
161
- theme: 'light' | 'dark';
162
- fontSize: number;
163
- notifications: boolean;
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 { userSettings } from './userSettings';
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
- return () => {
200
- clearTimeout(timer.current);
201
- };
202
- }, [state, updateUrl]);
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
- <h2>User Settings</h2>
207
- <p>Theme: {state.theme}</p>
208
- <p>Font Size: {state.fontSize}px</p>
209
- <button onClick={toggleTheme}>Toggle Theme</button>
210
- {/* Other UI elements to update other settings */}
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
- return (
221
- <div>
222
- <p>Notifications is {state.notifications ? 'On' : 'Off'}</p>
223
- </div>
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,u){const{parse:w,stringify:f}=r(a),{state:l,getState:d,setState:h}=o(a,(()=>s()?e(function(t,e){const n=Object.keys(t),o=Object.fromEntries(Object.entries(e||{}).filter((([t])=>n.includes(t))));return o}(a,u),a):w(c(a))));n((()=>{const t=()=>{const t=w(c(a));h(t)};return window.addEventListener(i,t),()=>{window.removeEventListener(i,t)}}),[h]);const p=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(d()):t?{...d(),...t}:d(),c=f(s,r);h(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,f,d]);return{updateState:h,updateUrl:p,state:l,getState:d}}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};
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.0",
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": [