state-in-url 6.2.4 → 6.4.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.
Files changed (51) hide show
  1. package/README.CN.md +4 -776
  2. package/README.es.md +836 -0
  3. package/README.fr.md +836 -0
  4. package/README.ja.md +836 -0
  5. package/{README.KO.md → README.ko.md} +69 -8
  6. package/README.md +34 -3
  7. package/README.pt-BR.md +836 -0
  8. package/README.ru.md +836 -0
  9. package/README.vi.md +836 -0
  10. package/README.zh-CN.md +836 -0
  11. package/dist/encoder/encoder.d.ts +1 -1
  12. package/dist/encoder/encoder.js +1 -1
  13. package/dist/encoder/encoder.mjs +1 -1
  14. package/dist/index.d.ts +9 -8
  15. package/dist/index.js +1 -1
  16. package/dist/index.mjs +1 -1
  17. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  18. package/dist/next/useUrlState/useUrlState.js +1 -1
  19. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  20. package/dist/package.json +34 -13
  21. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  22. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  23. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  24. package/dist/useInsertionEffect.js +1 -1
  25. package/dist/useLinkProps/hrefWithState.d.ts +3 -0
  26. package/dist/useLinkProps/hrefWithState.js +1 -0
  27. package/dist/useLinkProps/hrefWithState.mjs +1 -0
  28. package/dist/useLinkProps/index.d.ts +1 -0
  29. package/dist/useLinkProps/index.js +1 -0
  30. package/dist/useLinkProps/index.mjs +1 -0
  31. package/dist/useLinkProps/useLinkProps.d.ts +39 -0
  32. package/dist/useLinkProps/useLinkProps.js +1 -0
  33. package/dist/useLinkProps/useLinkProps.mjs +1 -0
  34. package/dist/useSharedState/useSharedState.d.ts +1 -1
  35. package/dist/useSharedState/useSharedState.js +1 -1
  36. package/dist/useSharedState/useSharedState.mjs +1 -1
  37. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  38. package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
  39. package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
  40. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  41. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  42. package/dist/utils.d.ts +12 -0
  43. package/dist/utils.js +1 -1
  44. package/dist/utils.mjs +1 -1
  45. package/package.json +34 -13
  46. package/skills/feature-state-hook/SKILL.md +36 -9
  47. package/skills/form-library-integration/SKILL.md +3 -4
  48. package/skills/input-handling/SKILL.md +20 -6
  49. package/skills/nextjs-ssr/SKILL.md +77 -9
  50. package/skills/react-router-remix-setup/SKILL.md +1 -1
  51. package/skills/shared-state-no-url/SKILL.md +1 -1
package/README.vi.md ADDED
@@ -0,0 +1,836 @@
1
+ <!-- i18n:start -->
2
+ [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md) · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · Tiếng Việt
3
+ <!-- i18n:meta locale=vi source=README.md source-blob=bbf45a10aae9e88f68b27d84f51ee3e7e8a1e436 status=translated -->
4
+ <!-- i18n:end -->
5
+
6
+ <div align="center">
7
+ <img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="logo state-in-url" width="120px" />
8
+
9
+ # State in url
10
+ </div>
11
+
12
+ <div align="center">
13
+ </div>
14
+
15
+ <div align="center">
16
+
17
+ [![Available for hire](https://img.shields.io/badge/available%20for%20hire-senior%20react%20engineer-2ea44f?style=flat-square)](https://asmyshlyaev177.dev)
18
+
19
+ [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
20
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
21
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
22
+
23
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
24
+ [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
25
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
26
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
27
+
28
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/asmyshlyaev177/state-in-url/badge)](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
29
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](https://www.bestpractices.dev/projects/9679)
30
+ [![license](https://img.shields.io/github/license/asmyshlyaev177/state-in-url.svg?style=flat-square)](https://github.com/asmyshlyaev177/state-in-url/blob/master/LICENSE)
31
+ <!-- or by embedding this in your HTML:
32
+ <a href="https://www.bestpractices.dev/projects/9679"><img src="https://www.bestpractices.dev/projects/9679/badge"></a> -->
33
+
34
+ </div>
35
+
36
+ <div align="center">
37
+
38
+ <h4 align="center">Đừng ngần ngại mở một issue nếu bạn tìm thấy bug, hoặc để yêu cầu tính năng</h4>
39
+
40
+ ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
41
+
42
+ # Demo
43
+
44
+ <a href="https://state-in-url.dev" target="_blank">Demo</a> |
45
+ <a href="https://state-in-url.netlify.app/" target="_blank">Liên kết dự phòng</a>
46
+
47
+ <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">Giới hạn kích thước URI, <b>tối đa 12KB</b> là an toàn</a>
48
+
49
+ <hr />
50
+
51
+ Hãy thêm một <a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a> và <a href="https://github.com/asmyshlyaev177" target="_blank">theo dõi tôi</a> để ủng hộ dự án!
52
+
53
+ Rất trân trọng phản hồi/ý kiến của bạn trên [thảo luận](https://github.com/asmyshlyaev177/state-in-url/discussions/1)
54
+
55
+ Hãy chia sẻ nếu nó hữu ích cho bạn.
56
+ [X.com](https://twitter.com/intent/tweet?&url=https://github.com/asmyshlyaev177/state-in-url)
57
+ [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
58
+ [FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
59
+ [VK](http://vk.com/share.php?url=https://github.com/asmyshlyaev177/state-in-url)
60
+
61
+ <hr />
62
+
63
+ </div>
64
+
65
+ [Cho tôi xem code ngay!](#useurlstate)
66
+
67
+ ## Tại sao dùng `state-in-url`?
68
+
69
+ Lưu bất kỳ state người dùng nào trong tham số truy vấn; hãy tưởng tượng JSON trong URL của trình duyệt. Tất cả đều giữ nguyên kiểu và cấu trúc dữ liệu, ví dụ số sẽ được giải mã thành số chứ không phải chuỗi, ngày thành ngày, v.v., hỗ trợ object và array.
70
+ Cực kỳ đơn giản, nhanh và có kiểm tra TypeScript tĩnh. Deep link, hay còn gọi là đồng bộ URL, trở nên dễ dàng.
71
+
72
+ Bao gồm hook `useUrlState` cho Next.js và react-router, cùng các helper cho mọi thứ khác trên JS.
73
+ Vì các trình duyệt hiện đại hỗ trợ URL khổng lồ và người dùng không quan tâm đến chuỗi truy vấn (đó là quy trình chọn tất cả và sao chép/dán).
74
+
75
+ Đã đến lúc dùng chuỗi truy vấn cho việc quản lý state, như mục đích ban đầu của nó.
76
+ Thư viện này làm mọi việc tẻ nhạt cho bạn.
77
+
78
+ Thư viện này là một lựa chọn thay thế tốt cho NUQS.
79
+
80
+ ### Trường hợp sử dụng
81
+
82
+ - Lưu biểu mẫu người dùng chưa lưu hoặc bộ lọc trang trong URL
83
+ - Đồng bộ URL với state React
84
+ - Chỉ đồng bộ dữ liệu giữa các client component không liên quan mà không chạm vào URI
85
+ - URL có thể chia sẻ kèm state ứng dụng (deep link, đồng bộ state trong URL)
86
+ - Dễ dàng duy trì state qua các lần tải lại trang
87
+
88
+ ### Tính năng
89
+
90
+ - 🧩 **Đơn giản**: Không provider, reducer, boilerplate hay khái niệm mới, API tương tự `React.useState`
91
+ - 📘 **Kiểm tra/tự hoàn thành TypeScript**: State chỉ là một object, kiểm tra tĩnh tự động trong IDE/test theo định nghĩa TypeScript
92
+ - ✨ **Dữ liệu phức tạp**: Object lồng nhau, ngày và array, hoạt động giống JSON nhưng trong URL
93
+ - ☂ **Giá trị mặc định**: Cung cấp giá trị mặc định nếu tham số không có trong url
94
+ - ⌨ **Có tổ chức**: Tất cả giá trị có thể được định nghĩa ngay từ đầu, bảo vệ bạn khỏi việc lấy key không tồn tại
95
+ - **tương thích**: Sẽ giữ nguyên các tham số truy vấn của bên thứ ba
96
+ - **linh hoạt**: Có thể dùng nhiều hơn 1 object state trên cùng một trang, chỉ cần dùng key khác nhau
97
+ - **Nhanh**: Re-render tối thiểu, khoảng [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) để mã hóa và giải mã object lớn
98
+ - **Server Side Rendering**: Có thể dùng trong Server Component, hỗ trợ Next.js 14, 15 và 16
99
+ - **Nhẹ**: Không phụ thuộc, thư viện dưới 2KB
100
+ - **DX**: Trải nghiệm lập trình tốt, tài liệu, chú thích JSDoc và ví dụ
101
+ - **Linh hoạt framework**: Hook cho `Next.js` và `react-router`, helper để dùng với framework khác hoặc JS thuần
102
+ - **Được kiểm thử kỹ**: [Unit test và Playwright test cho Chrome/Firefox/Safari](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
+ - **Giấy phép thoáng**: MIT
104
+
105
+ ## Mục lục
106
+
107
+ <!-- toc:start -->
108
+
109
+ - [State in url](#state-in-url)
110
+ - [Demo](#demo)
111
+ - [Tại sao dùng `state-in-url`?](#tại-sao-dùng-state-in-url)
112
+ - [Trường hợp sử dụng](#trường-hợp-sử-dụng)
113
+ - [Tính năng](#tính-năng)
114
+ - [Mục lục](#mục-lục)
115
+ - [Cài đặt](#cài-đặt)
116
+ - [1. Cài đặt package](#1-cài-đặt-package)
117
+ - [2. Sửa tsconfig.json](#2-sửa-tsconfigjson)
118
+ - [Dùng với AI coding agent](#dùng-với-ai-coding-agent)
119
+ - [useUrlState](#useurlstate)
120
+ - [Hook useUrlState cho Next.js](#hook-useurlstate-cho-nextjs)
121
+ - [Ví dụ sử dụng](#ví-dụ-sử-dụng)
122
+ - [Cơ bản](#cơ-bản)
123
+ - [Với server side rendering](#với-server-side-rendering)
124
+ - [Dùng hook trong component `layout`](#dùng-hook-trong-component-layout)
125
+ - [Với hình dạng state tùy ý (không khuyến nghị)](#với-hình-dạng-state-tùy-ý-không-khuyến-nghị)
126
+ - [Hook useUrlState cho Remix.js](#hook-useurlstate-cho-remixjs)
127
+ - [Ví dụ](#ví-dụ)
128
+ - [Hook useUrlState cho React-Router](#hook-useurlstate-cho-react-router)
129
+ - [Ví dụ](#ví-dụ-1)
130
+ - [Công thức nấu ăn](#công-thức-nấu-ăn)
131
+ - [Hook tùy chỉnh để làm việc thuận tiện với một phần của state](#hook-tùy-chỉnh-để-làm-việc-thuận-tiện-với-một-phần-của-state)
132
+ - [Với hình dạng state phức tạp](#với-hình-dạng-state-phức-tạp)
133
+ - [Chỉ cập nhật state và đồng bộ với URL thủ công](#chỉ-cập-nhật-state-và-đồng-bộ-với-url-thủ-công)
134
+ - [Các hook và helper khác](#các-hook-và-helper-khác)
135
+ - [Hook `useUrlStateBase` cho các router khác](#hook-useurlstatebase-cho-các-router-khác)
136
+ - [Hook `useSharedState` cho React.js](#hook-usesharedstate-cho-reactjs)
137
+ - [Hook `useLinkProps` cho React.js](#hook-uselinkprops-cho-reactjs)
138
+ - [Hook `useUrlEncode` cho React.js](#hook-useurlencode-cho-reactjs)
139
+ - [Helper `encodeState` và `decodeState`](#helper-encodestate-và-decodestate)
140
+ - [Helper `encode` và `decode`](#helper-encode-và-decode)
141
+ - [Thực hành tốt](#thực-hành-tốt)
142
+ - [Lưu ý](#lưu-ý)
143
+ - [Khác](#khác)
144
+ - [Đóng góp và/hoặc chạy cục bộ](#đóng-góp-vàhoặc-chạy-cục-bộ)
145
+ - [Lộ trình](#lộ-trình)
146
+ - [Liên hệ & Hỗ trợ](#liên-hệ--hỗ-trợ)
147
+ - [Changelog](#changelog)
148
+ - [Được nhắc đến](#được-nhắc-đến)
149
+ - [Giấy phép](#giấy-phép)
150
+ - [Thuê tôi](#thuê-tôi)
151
+ - [Cảm hứng](#cảm-hứng)
152
+
153
+ <!-- toc:end -->
154
+
155
+ ## Cài đặt
156
+
157
+ ### 1. Cài đặt package
158
+
159
+ ```sh
160
+ # npm
161
+ npm install --save state-in-url
162
+ # yarn
163
+ yarn add state-in-url
164
+ # pnpm
165
+ pnpm add state-in-url
166
+ ```
167
+
168
+ ### 2. Sửa tsconfig.json
169
+
170
+ Trong `tsconfig.json`, trong `compilerOptions`, đặt `"moduleResolution": "Bundler"`, hoặc `"moduleResolution": "Node16"`, hoặc `"moduleResolution": "NodeNext"`.
171
+ Có thể cần đặt `"module": "ES2022"`, hoặc `"module": "ESNext"`
172
+
173
+ ## Dùng với AI coding agent
174
+
175
+ `state-in-url` kèm theo các file skill cho [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview), để các AI agent (Claude Code, Cursor, Copilot, Codex, v.v.) tải đúng pattern và tránh các lỗi thường gặp khi dùng thư viện. Sau khi cài `state-in-url`, chạy một lần trong dự án của bạn:
176
+
177
+ ```sh
178
+ npx @tanstack/intent@latest install
179
+ ```
180
+
181
+ Điều này cấu hình agent đã cài của bạn để khám phá các skill từ `node_modules/state-in-url/skills/`. Liệt kê các skill có sẵn bằng `npx @tanstack/intent@latest list`.
182
+
183
+ ## useUrlState
184
+
185
+ Hook chính nhận state ban đầu làm tham số và trả về object state, callback để cập nhật url và callback để chỉ cập nhật state.
186
+ Tất cả component dùng cùng object `state` sẽ được đồng bộ tự động.
187
+
188
+ ### Hook useUrlState cho Next.js
189
+
190
+ [Tài liệu API đầy đủ](packages/urlstate/next/useUrlState)
191
+
192
+ [Ví dụ React-Router](#hook-useurlstate-cho-react-router)
193
+
194
+ #### Ví dụ sử dụng
195
+
196
+ ##### Cơ bản
197
+
198
+ 1. Định nghĩa hình dạng state với các giá trị mặc định
199
+
200
+ ```typescript
201
+ // userState.ts
202
+ // Chỉ các tham số có giá trị khác mặc định mới được đưa vào url.
203
+ export const userState: UserState = { name: '', age: 0 }
204
+
205
+ // Dùng `Type` chứ không phải `Interface`!
206
+ type UserState = { name: string, age: number }
207
+ ```
208
+
209
+ 2. Import và sử dụng
210
+
211
+ ```typescript
212
+ 'use client'
213
+ import { useUrlState } from 'state-in-url/next';
214
+
215
+ import { userState } from './userState';
216
+
217
+ function MyComponent() {
218
+ // có thể truyền đối số `replace`, nó kiểm soát việc `setUrl` sẽ dùng `router.push` hay `router.replace`, mặc định replace=true
219
+ // có thể truyền `searchParams` từ server component, truyền `useHistory: false` nếu bạn cần fetch thứ gì đó trong server component
220
+ const { urlState, setUrl, setState } = useUrlState(userState);
221
+
222
+ return (
223
+ <div>
224
+ // urlState.name sẽ trả về giá trị mặc định từ `userState` nếu url trống
225
+ <input value={urlState.name}
226
+ // api giống React.useState, ví dụ setUrl(currVal => currVal + 1)
227
+ onChange={(ev) => setUrl({ name: ev.target.value }) }
228
+ />
229
+ <input value={urlState.age}
230
+ onChange={(ev) => setUrl({ age: +ev.target.value }) }
231
+ />
232
+
233
+ <input value={urlState.name}
234
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
235
+ // Có thể cập nhật state ngay lập tức nhưng đồng bộ thay đổi với url khi cần
236
+ onBlur={() => setUrl()}
237
+ />
238
+
239
+ <button onClick={() => setUrl((_, initial) => initial)}>
240
+ Reset
241
+ </button>
242
+
243
+ </div>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ##### Với server side rendering
249
+
250
+ <details>
251
+ <Summary>Ví dụ</Summary>
252
+
253
+ ```typescript
254
+ export default async function Home({ searchParams }: { searchParams: object }) {
255
+ return (
256
+ <Form searchParams={searchParams} />
257
+ )
258
+ }
259
+
260
+ // Form.tsx
261
+ 'use client'
262
+ import React from 'react';
263
+ import { useUrlState } from 'state-in-url/next';
264
+ import { form } from './form';
265
+
266
+ const Form = ({ searchParams }: { searchParams: object }) => {
267
+ const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
268
+ }
269
+ ```
270
+
271
+ </details>
272
+
273
+ ##### Dùng hook trong component `layout`
274
+
275
+ <details>
276
+ <Summary>Ví dụ</Summary>
277
+ Đó là một phần khó, vì nextjs với app router không cho phép truy cập searchParams từ phía server. Có một cách giải quyết bằng middleware, nhưng nó không đẹp và có thể ngừng hoạt động sau khi nextjs cập nhật.
278
+
279
+ ```typescript
280
+ // thêm vào `layout.tsx` phù hợp
281
+ export const runtime = 'edge';
282
+
283
+ // middleware.ts
284
+ import type { NextRequest } from 'next/server';
285
+ import { NextResponse } from 'next/server';
286
+
287
+ export function middleware(request: NextRequest) {
288
+ const url = request.url?.includes('_next') ? null : request.url;
289
+ const sp = url?.split?.('?')?.[1] || '';
290
+
291
+ const response = NextResponse.next();
292
+
293
+ if (url !== null) {
294
+ response.headers.set('searchParams', sp);
295
+ }
296
+
297
+ return response;
298
+ }
299
+
300
+ // Component layout mục tiêu
301
+ import { headers } from 'next/headers';
302
+ import { decodeState } from 'state-in-url/encodeState';
303
+
304
+ export default async function Layout({
305
+ children,
306
+ }: {
307
+ children: React.ReactNode;
308
+ }) {
309
+ const sp = headers().get('searchParams') || '';
310
+
311
+ return (
312
+ <div>
313
+ <Comp1 searchParams={decodeState(sp, stateShape)} />
314
+ {children}
315
+ </div>
316
+ );
317
+ }
318
+
319
+
320
+ ```
321
+
322
+ </details>
323
+
324
+ ##### Với hình dạng state tùy ý (không khuyến nghị)
325
+
326
+ <details>
327
+ <Summary>Ví dụ</Summary>
328
+
329
+ ```typescript
330
+ 'use client'
331
+ import { useUrlState } from 'state-in-url/next';
332
+
333
+ const someObj = {};
334
+
335
+ function SettingsComponent() {
336
+ const { urlState, setUrl, setState } = useUrlState<object>(someObj);
337
+ }
338
+ ```
339
+
340
+ </details>
341
+
342
+ ### Hook useUrlState cho Remix.js
343
+
344
+ API giống với phiên bản Next.js, ngoại trừ có thể truyền option từ type [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
345
+
346
+ [Tài liệu API](packages/urlstate/remix/useUrlState)
347
+
348
+ #### Ví dụ
349
+
350
+ ```typescript
351
+ export const form: Form = {
352
+ name: '',
353
+ age: undefined,
354
+ agree_to_terms: false,
355
+ tags: [],
356
+ };
357
+
358
+ type Form = {
359
+ name: string;
360
+ age?: number;
361
+ agree_to_terms: boolean;
362
+ tags: { id: string; value: { text: string; time: Date } }[];
363
+ };
364
+
365
+ ```
366
+
367
+ ```typescript
368
+ import { useUrlState } from 'state-in-url/remix';
369
+
370
+ import { form } from './form';
371
+
372
+ function TagsComponent() {
373
+ const { urlState, setUrl, setState } = useUrlState(form);
374
+
375
+ const onChangeTags = React.useCallback(
376
+ (tag: (typeof tags)[number]) => {
377
+ setUrl((curr) => ({
378
+ ...curr,
379
+ tags: curr.tags.find((t) => t.id === tag.id)
380
+ ? curr.tags.filter((t) => t.id !== tag.id)
381
+ : curr.tags.concat(tag),
382
+ }));
383
+ },
384
+ [setUrl],
385
+ );
386
+
387
+ return (
388
+ <div>
389
+ <Field text="Tags">
390
+ <div className="flex flex-wrap gap-2">
391
+ {tags.map((tag) => (
392
+ <Tag
393
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
394
+ text={tag.value.text}
395
+ onClick={() => onChangeTags(tag)}
396
+ key={tag.id}
397
+ />
398
+ ))}
399
+ </div>
400
+ </Field>
401
+
402
+ <input value={urlState.name}
403
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
404
+ // Có thể cập nhật state ngay lập tức nhưng đồng bộ thay đổi với url khi cần
405
+ onBlur={() => setUrl()}
406
+ />
407
+ </div>
408
+ );
409
+ }
410
+
411
+ const tags = [
412
+ {
413
+ id: '1',
414
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
415
+ },
416
+ {
417
+ id: '2',
418
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
419
+ },
420
+ {
421
+ id: '3',
422
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
423
+ },
424
+ ];
425
+ ```
426
+
427
+ [Ví dụ code](packages/example-remix2/app/routes/Form-for-test.tsx)
428
+
429
+ ### Hook useUrlState cho React-Router
430
+
431
+ API giống với phiên bản Next.js, ngoại trừ có thể truyền option từ type [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
432
+
433
+ [Tài liệu API](packages/urlstate/react-router/useUrlState)
434
+
435
+ #### Ví dụ
436
+
437
+ ```typescript
438
+ export const form: Form = {
439
+ name: '',
440
+ age: undefined,
441
+ agree_to_terms: false,
442
+ tags: [],
443
+ };
444
+
445
+ type Form = {
446
+ name: string;
447
+ age?: number;
448
+ agree_to_terms: boolean;
449
+ tags: { id: string; value: { text: string; time: Date } }[];
450
+ };
451
+
452
+ ```
453
+
454
+ ```typescript
455
+ import { useUrlState } from 'state-in-url/react-router';
456
+ // cho react-router v6
457
+ // import { useUrlState } from 'state-in-url/react-router6';
458
+
459
+ import { form } from './form';
460
+
461
+ function TagsComponent() {
462
+ const { urlState, setUrl, setState } = useUrlState(form);
463
+
464
+ const onChangeTags = React.useCallback(
465
+ (tag: (typeof tags)[number]) => {
466
+ setUrl((curr) => ({
467
+ ...curr,
468
+ tags: curr.tags.find((t) => t.id === tag.id)
469
+ ? curr.tags.filter((t) => t.id !== tag.id)
470
+ : curr.tags.concat(tag),
471
+ }));
472
+ },
473
+ [setUrl],
474
+ );
475
+
476
+ return (
477
+ <div>
478
+ <Field text="Tags">
479
+ <div className="flex flex-wrap gap-2">
480
+ {tags.map((tag) => (
481
+ <Tag
482
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
483
+ text={tag.value.text}
484
+ onClick={() => onChangeTags(tag)}
485
+ key={tag.id}
486
+ />
487
+ ))}
488
+ </div>
489
+ </Field>
490
+
491
+ <input value={urlState.name}
492
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
493
+ // Có thể cập nhật state ngay lập tức nhưng đồng bộ thay đổi với url khi cần
494
+ onBlur={() => setUrl()}
495
+ />
496
+ </div>
497
+ );
498
+ }
499
+
500
+ const tags = [
501
+ {
502
+ id: '1',
503
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
504
+ },
505
+ {
506
+ id: '2',
507
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
508
+ },
509
+ {
510
+ id: '3',
511
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
512
+ },
513
+ ];
514
+ ```
515
+
516
+ [Ví dụ code](packages/example-react-router6/src/Form-for-test.tsx)
517
+
518
+ ## Công thức nấu ăn
519
+ ##### Hook tùy chỉnh để làm việc thuận tiện với một phần của state
520
+ <details>
521
+ <Summary>Ví dụ</Summary>
522
+
523
+ ```typescript
524
+ 'use client';
525
+
526
+ import React from 'react';
527
+ import { useUrlState } from 'state-in-url/next';
528
+
529
+ const form: Form = {
530
+ name: '',
531
+ age: undefined,
532
+ agree_to_terms: false,
533
+ tags: [],
534
+ };
535
+
536
+ type Form = {
537
+ name: string;
538
+ age?: number;
539
+ agree_to_terms: boolean;
540
+ tags: {id: string; value: {text: string; time: Date } }[];
541
+ };
542
+
543
+ export const useFormState = ({ searchParams }: { searchParams?: object }) => {
544
+ const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
545
+ searchParams,
546
+ });
547
+
548
+ // lần điều hướng đầu tiên sẽ push một mục lịch sử mới
549
+ // tất cả các lần sau sẽ chỉ replace mục đó
550
+ // theo cách này lịch sử sẽ chỉ có 2 mục - ['/url', '/url?key=param']
551
+
552
+ const replace = React.useRef(false);
553
+ const setUrl = React.useCallback((
554
+ state: Parameters<typeof setUrlBase>[0],
555
+ opts?: Parameters<typeof setUrlBase>[1]
556
+ ) => {
557
+ setUrlBase(state, { replace: replace.current, ...opts });
558
+ replace.current = true;
559
+ }, [setUrlBase]);
560
+
561
+ return { urlState, setUrl, resetUrl: reset };
562
+ };
563
+ ```
564
+ </details>
565
+
566
+ <hr />
567
+
568
+ ##### Với hình dạng state phức tạp
569
+
570
+ <details>
571
+ <Summary>Ví dụ</Summary>
572
+
573
+ ```typescript
574
+ export const form: Form = {
575
+ name: '',
576
+ age: undefined,
577
+ agree_to_terms: false,
578
+ tags: [],
579
+ };
580
+
581
+ type Form = {
582
+ name: string;
583
+ age?: number;
584
+ agree_to_terms: boolean;
585
+ tags: { id: string; value: { text: string; time: Date } }[];
586
+ };
587
+ ```
588
+
589
+ ```typescript
590
+ 'use client'
591
+ import { useUrlState } from 'state-in-url/next';
592
+
593
+ import { form } from './form';
594
+
595
+ function TagsComponent() {
596
+ // `urlState` sẽ được suy ra từ type Form!
597
+ const { urlState, setUrl } = useUrlState(form);
598
+
599
+ const onChangeTags = React.useCallback(
600
+ (tag: (typeof tags)[number]) => {
601
+ setUrl((curr) => ({
602
+ ...curr,
603
+ tags: curr.tags.find((t) => t.id === tag.id)
604
+ ? curr.tags.filter((t) => t.id !== tag.id)
605
+ : curr.tags.concat(tag),
606
+ }));
607
+ },
608
+ [setUrl],
609
+ );
610
+
611
+ return (
612
+ <div>
613
+ <Field text="Tags">
614
+ <div className="flex flex-wrap gap-2">
615
+ {tags.map((tag) => (
616
+ <Tag
617
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
618
+ text={tag.value.text}
619
+ onClick={() => onChangeTags(tag)}
620
+ key={tag.id}
621
+ />
622
+ ))}
623
+ </div>
624
+ </Field>
625
+ </div>
626
+ );
627
+ }
628
+
629
+ const tags = [
630
+ {
631
+ id: '1',
632
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
633
+ },
634
+ {
635
+ id: '2',
636
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
637
+ },
638
+ {
639
+ id: '3',
640
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
641
+ },
642
+ ];
643
+ ```
644
+
645
+ [Ví dụ code trang demo](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
646
+ </details>
647
+
648
+ ##### Chỉ cập nhật state và đồng bộ với URL thủ công
649
+
650
+ <details>
651
+ <Summary>Ví dụ</Summary>
652
+
653
+ ```typescript
654
+
655
+ const timer = React.useRef(0 as unknown as NodeJS.Timeout);
656
+ React.useEffect(() => {
657
+ clearTimeout(timer.current);
658
+ timer.current = setTimeout(() => {
659
+ // sẽ so sánh state theo nội dung chứ không phải theo tham chiếu và chỉ kích hoạt cập nhật cho các giá trị mới
660
+ setUrl(urlState);
661
+ }, 500);
662
+
663
+ return () => {
664
+ clearTimeout(timer.current);
665
+ };
666
+ }, [urlState, setUrl]);
667
+ ```
668
+
669
+ Đồng bộ state trong `onBlur` sẽ phù hợp hơn với cách dùng thực tế.
670
+
671
+ ```typescript
672
+ <input onBlur={() => updateUrl()} .../>
673
+ ```
674
+
675
+ </details>
676
+
677
+ ## Các hook và helper khác
678
+
679
+ ### Hook `useUrlStateBase` cho các router khác
680
+
681
+ Hook để tạo các hook `useUrlState` của riêng bạn với các router khác, ví dụ react-router hoặc tanstack router.
682
+
683
+ [Tài liệu API](packages/urlstate/useUrlStateBase)
684
+
685
+ ### Hook `useSharedState` cho React.js
686
+
687
+ Hook để chia sẻ state giữa bất kỳ component React nào, đã thử nghiệm với Next.js và Vite.
688
+
689
+ ```typescript
690
+ 'use client'
691
+ import { useSharedState } from 'state-in-url';
692
+
693
+ export const someState = { name: '' };
694
+
695
+ function SettingsComponent() {
696
+ const { state, setState } = useSharedState(someState);
697
+ }
698
+ ```
699
+
700
+ [Tài liệu API](packages/urlstate/useSharedState/README.vi.md)
701
+
702
+ ### Hook `useLinkProps` cho React.js
703
+
704
+ Hook mang trạng thái sang một liên kết trỏ tới route khác, ví dụ bộ chuyển ngôn ngữ. `setUrl` luôn ghi vào đường dẫn hiện tại, hook này thì không.
705
+
706
+ ```tsx
707
+ 'use client'
708
+ import Link from 'next/link';
709
+ import { useRouter } from 'next/navigation';
710
+ import { useLinkProps } from 'state-in-url/useLinkProps';
711
+
712
+ export const form = { name: '' };
713
+
714
+ function LanguagePicker() {
715
+ const linkProps = useLinkProps(form, useRouter().push);
716
+
717
+ return <Link {...linkProps('/de/pricing')}>Deutsch</Link>;
718
+ }
719
+ ```
720
+
721
+ Phần đánh dấu vẫn giữ `href` nguyên bản, nên trình thu thập dữ liệu và `hreflang` thấy URL chuẩn tắc; trạng thái được đọc lúc nhấp chuột.
722
+
723
+ [Tài liệu API](packages/urlstate/useLinkProps/README.vi.md)
724
+
725
+ ### Hook `useUrlEncode` cho React.js
726
+
727
+ [Tài liệu API](packages/urlstate/useUrlEncode/README.vi.md)
728
+
729
+ ### Helper `encodeState` và `decodeState`
730
+
731
+ [Tài liệu API](packages/urlstate/encodeState/README.vi.md)
732
+
733
+ ### Helper `encode` và `decode`
734
+
735
+ [Tài liệu API](packages/urlstate/encoder/README.vi.md)
736
+
737
+ ## Thực hành tốt
738
+
739
+ - Định nghĩa hình dạng state của bạn là một hằng số
740
+ - Dùng TypeScript để tăng an toàn kiểu và tự hoàn thành
741
+ - Tránh lưu thông tin nhạy cảm trong tham số URL (SSN, khóa API, v.v.)
742
+ - Dùng [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) này để có lỗi TS dễ đọc
743
+
744
+ Bạn có thể tạo các state hook cho từng phần của state và tái sử dụng chúng trong toàn ứng dụng. Ví dụ:
745
+ ```Typescript
746
+ type UserState = {
747
+ name: string;
748
+ age: number;
749
+ other: { id: string, value: number }[]
750
+ };
751
+ const userState = {
752
+ name: '',
753
+ age: 0,
754
+ other: [],
755
+ };
756
+
757
+ export const useUserState = () => {
758
+ const { urlState, setUrl, reset } = useUrlState(userState);
759
+
760
+ // logic khác
761
+
762
+ // đặt lại tham số truy vấn khi điều hướng sang trang khác
763
+ React.useEffect(() => {
764
+ return reset
765
+ }, [])
766
+
767
+ return { userState: urlState, setUserState: setUrl };;
768
+ }
769
+
770
+ ```
771
+
772
+ ## Lưu ý
773
+
774
+ 1. Chỉ có thể truyền các giá trị tuần tự hóa được; `Function`, `BigInt` hoặc `Symbol` sẽ không hoạt động, và có lẽ cả những thứ như `ArrayBuffer` cũng vậy. Mọi thứ có thể tuần tự hóa thành JSON sẽ hoạt động.
775
+ 2. Máy chủ Vercel giới hạn kích thước header (chuỗi truy vấn và những thứ khác) ở **14KB**, vì vậy hãy giữ state trong URL dưới ~5000 từ. <https://vercel.com/docs/errors/URL_TOO_LONG>
776
+ 3. Đã thử nghiệm với `next.js` 14/15/16 với app router, không có kế hoạch hỗ trợ pages.
777
+
778
+ ## Khác
779
+
780
+ ### Đóng góp và/hoặc chạy cục bộ
781
+
782
+ Xem [tài liệu đóng góp](CONTRIBUTING.md)
783
+
784
+ ## Lộ trình
785
+
786
+ - [x] hook cho `Next.js`
787
+ - [x] hook cho `react-router`
788
+ - [x] hook cho `remix`
789
+ - [ ] hook cho `svelte`
790
+ - [ ] hook cho `astro`
791
+ - [ ] hook để lưu state trong hash ?
792
+
793
+ ## Liên hệ & Hỗ trợ
794
+
795
+ - Tạo một [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) cho báo cáo lỗi, yêu cầu tính năng hoặc câu hỏi
796
+
797
+ ## [Changelog](CHANGELOG.md)
798
+
799
+ ## Được nhắc đến
800
+
801
+ - [This Week in React 209](https://thisweekinreact.com/newsletter/209)
802
+ - [JavaScript Weekly](https://javascriptweekly.com/issues/741)
803
+ - [This Week in React 240](https://thisweekinreact.com/newsletter/240)
804
+
805
+ ## Giấy phép
806
+
807
+ Dự án này được cấp phép theo [giấy phép MIT](LICENSE).
808
+
809
+ ## Thuê tôi
810
+
811
+ Tôi là **Aleksandr Smyshliaev** — tác giả và người bảo trì duy nhất của thư viện này.
812
+ Kỹ sư frontend cấp cao (React / Next.js / TypeScript, hơn 8 năm), và **sẵn sàng cho công việc remote toàn thời gian ngay bây giờ**.
813
+
814
+ Thư viện này là phiên bản ngắn gọn của những gì tôi giỏi: một API có kiểu trên một nguyên thủy trình duyệt lộn xộn, không phụ thuộc và ổn định trên Next.js, Remix và React Router qua nhiều phiên bản React chính.
815
+
816
+ - **Giỏi nhất ở** — thư viện component, quản lý state và bộ test sống sót qua refactor.
817
+ - **Cũng là của tôi** —
818
+ [react-horizontal-scrolling-menu](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu)
819
+ (~84k lượt cài mỗi tuần),
820
+ [test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
821
+ (ghi/phát lại cho Playwright),
822
+ [llm-queue](https://github.com/asmyshlyaev177/llm-queue).
823
+ - **Ở đâu** — Tbilisi, Georgia (GMT+4), trùng hoàn toàn với CET. Đã đăng ký thực thể nhà thầu, nên hợp đồng B2B không cần thiết lập employer-of-record.
824
+ - **Liên hệ tôi** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
825
+ [asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
826
+ [LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
827
+
828
+ ## Cảm hứng
829
+
830
+ [NUQS](https://github.com/47ng/nuqs)
831
+
832
+ [Dùng URL để lưu state trong Vue](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
833
+
834
+ [Lưu state trong URL](https://antonz.org/storing-state/)
835
+
836
+ [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)