state-in-url 6.3.0 → 6.4.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.
Files changed (44) 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/index.d.ts +9 -8
  12. package/dist/index.js +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/package.json +55 -35
  15. package/dist/useIsomorphicLayoutEffect.d.ts +2 -0
  16. package/dist/useIsomorphicLayoutEffect.js +1 -0
  17. package/dist/useIsomorphicLayoutEffect.mjs +1 -0
  18. package/dist/useLinkProps/hrefWithState.d.ts +3 -0
  19. package/dist/useLinkProps/hrefWithState.js +1 -0
  20. package/dist/useLinkProps/hrefWithState.mjs +1 -0
  21. package/dist/useLinkProps/index.d.ts +1 -0
  22. package/dist/useLinkProps/index.js +1 -0
  23. package/dist/useLinkProps/index.mjs +1 -0
  24. package/dist/useLinkProps/useLinkProps.d.ts +39 -0
  25. package/dist/useLinkProps/useLinkProps.js +1 -0
  26. package/dist/useLinkProps/useLinkProps.mjs +1 -0
  27. package/dist/useSharedState/useSharedState.js +1 -1
  28. package/dist/useSharedState/useSharedState.mjs +1 -1
  29. package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
  30. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  31. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  32. package/dist/utils.d.ts +2 -0
  33. package/dist/utils.js +1 -1
  34. package/dist/utils.mjs +1 -1
  35. package/package.json +55 -35
  36. package/skills/feature-state-hook/SKILL.md +15 -2
  37. package/skills/form-library-integration/SKILL.md +1 -1
  38. package/skills/input-handling/SKILL.md +1 -1
  39. package/skills/nextjs-ssr/SKILL.md +1 -1
  40. package/skills/react-router-remix-setup/SKILL.md +1 -1
  41. package/skills/shared-state-no-url/SKILL.md +1 -1
  42. package/dist/useInsertionEffect.d.ts +0 -2
  43. package/dist/useInsertionEffect.js +0 -1
  44. package/dist/useInsertionEffect.mjs +0 -1
package/README.fr.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 · [Tiếng Việt](./README.vi.md)
3
+ <!-- i18n:meta locale=fr 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 de 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">N'hésitez pas à ouvrir une issue si vous avez trouvé un bug, ou pour demander des fonctionnalités</h4>
39
+
40
+ ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
41
+
42
+ # Démo
43
+
44
+ <a href="https://state-in-url.dev" target="_blank">Démo</a> |
45
+ <a href="https://state-in-url.netlify.app/" target="_blank">Lien miroir</a>
46
+
47
+ <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">Limitation de la taille de l'URI, <b>jusqu'à 12KB</b> est sûr</a>
48
+
49
+ <hr />
50
+
51
+ Ajoutez une <a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a> et <a href="https://github.com/asmyshlyaev177" target="_blank">suivez-moi</a> pour soutenir le projet !
52
+
53
+ J'apprécierai vos retours/avis dans les [discussions](https://github.com/asmyshlyaev177/state-in-url/discussions/1)
54
+
55
+ Partagez si cela vous est utile.
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
+ [Montrez-moi juste le code !](#useurlstate)
66
+
67
+ ## Pourquoi utiliser `state-in-url` ?
68
+
69
+ Stockez n'importe quel état utilisateur dans les paramètres de requête ; imaginez du JSON dans une URL de navigateur. Tout cela en conservant les types et la structure des données : par exemple, les nombres seront décodés comme des nombres et non comme des chaînes, les dates comme des dates, etc., les objets et les tableaux sont pris en charge.
70
+ Extrêmement simple, rapide et avec une validation TypeScript statique. Les liens profonds, autrement dit la synchronisation d'URL, deviennent faciles.
71
+
72
+ Contient le hook `useUrlState` pour Next.js et react-router, ainsi que des helpers pour tout le reste en JS.
73
+ Comme les navigateurs modernes prennent en charge d'énormes URL et que les utilisateurs ne se soucient pas des chaînes de requête (c'est un flux de travail « tout sélectionner et copier/coller »).
74
+
75
+ Il est temps d'utiliser la chaîne de requête pour la gestion d'état, comme cela était prévu à l'origine.
76
+ Cette bibliothèque fait tout le travail ingrat à votre place.
77
+
78
+ Cette bibliothèque est une bonne alternative à NUQS.
79
+
80
+ ### Cas d'utilisation
81
+
82
+ - Stocker des formulaires utilisateur non enregistrés ou des filtres de page dans l'URL
83
+ - Synchroniser l'URL avec l'état de React
84
+ - Synchroniser simplement des données entre des composants client sans rapport, sans toucher à l'URI
85
+ - Des URL partageables avec l'état de l'application (liens profonds, synchronisation de l'état dans l'URL)
86
+ - Persistance simple de l'état entre les rechargements de page
87
+
88
+ ### Fonctionnalités
89
+
90
+ - 🧩 **Simple** : Pas de providers, reducers, code répétitif ni nouveaux concepts, une API similaire à `React.useState`
91
+ - 📘 **Validation/autocomplétion TypeScript** : L'état n'est qu'un objet, validation statique automatique dans l'IDE/les tests selon la définition TypeScript
92
+ - ✨ **Données complexes** : Objets imbriqués, dates et tableaux, fonctionne comme JSON, mais dans l'URL
93
+ - ☂ **Valeurs par défaut** : Vous donne des valeurs par défaut si le paramètre n'est pas dans l'url
94
+ - ⌨ **Organisé** : Toutes les valeurs possibles définies au départ, vous protège contre l'obtention d'une clé inexistante
95
+ - **compatible** : Conservera les paramètres de requête tiers tels quels
96
+ - **flexible** : Peut utiliser plus d'un objet d'état sur la même page, utilisez simplement des clés différentes
97
+ - **Rapide** : Re-rendus minimaux, environ [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) pour encoder et décoder un gros objet
98
+ - **Rendu côté serveur** : Utilisable dans les composants serveur, Next.js 14, 15 et 16 sont pris en charge
99
+ - **Léger** : Zéro dépendance, bibliothèque de moins de 2KB
100
+ - **DX** : Bonne expérience de développement, documentation, commentaires JSDoc et exemples
101
+ - **Flexibilité de frameworks** : Hooks pour `Next.js` et `react-router`, helpers pour l'utiliser avec d'autres frameworks ou du JS pur
102
+ - **Bien testée** : [Tests unitaires et tests Playwright pour Chrome/Firefox/Safari](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
+ - **Licence permissive** : MIT
104
+
105
+ ## Table des matières
106
+
107
+ <!-- toc:start -->
108
+
109
+ - [State in url](#state-in-url)
110
+ - [Démo](#démo)
111
+ - [Pourquoi utiliser `state-in-url` ?](#pourquoi-utiliser-state-in-url-)
112
+ - [Cas d'utilisation](#cas-dutilisation)
113
+ - [Fonctionnalités](#fonctionnalités)
114
+ - [Table des matières](#table-des-matières)
115
+ - [Installation](#installation)
116
+ - [1. Installer le paquet](#1-installer-le-paquet)
117
+ - [2. Modifier tsconfig.json](#2-modifier-tsconfigjson)
118
+ - [Utilisation avec des agents de codage IA](#utilisation-avec-des-agents-de-codage-ia)
119
+ - [useUrlState](#useurlstate)
120
+ - [Hook useUrlState pour Next.js](#hook-useurlstate-pour-nextjs)
121
+ - [Exemples d'utilisation](#exemples-dutilisation)
122
+ - [Basique](#basique)
123
+ - [Avec le rendu côté serveur](#avec-le-rendu-côté-serveur)
124
+ - [Utiliser le hook dans le composant `layout`](#utiliser-le-hook-dans-le-composant-layout)
125
+ - [Avec une forme d'état arbitraire (non recommandé)](#avec-une-forme-détat-arbitraire-non-recommandé)
126
+ - [Hook useUrlState pour Remix.js](#hook-useurlstate-pour-remixjs)
127
+ - [Exemple](#exemple)
128
+ - [Hook useUrlState pour React-Router](#hook-useurlstate-pour-react-router)
129
+ - [Exemple](#exemple-1)
130
+ - [Recettes](#recettes)
131
+ - [Hook personnalisé pour travailler confortablement avec une tranche d'état](#hook-personnalisé-pour-travailler-confortablement-avec-une-tranche-détat)
132
+ - [Avec une forme d'état complexe](#avec-une-forme-détat-complexe)
133
+ - [Mettre à jour l'état seul et le synchroniser manuellement avec l'URL](#mettre-à-jour-létat-seul-et-le-synchroniser-manuellement-avec-lurl)
134
+ - [Autres hooks et helpers](#autres-hooks-et-helpers)
135
+ - [Hook `useUrlStateBase` pour d'autres routeurs](#hook-useurlstatebase-pour-dautres-routeurs)
136
+ - [Hook `useSharedState` pour React.js](#hook-usesharedstate-pour-reactjs)
137
+ - [Hook `useLinkProps` pour React.js](#hook-uselinkprops-pour-reactjs)
138
+ - [Hook `useUrlEncode` pour React.js](#hook-useurlencode-pour-reactjs)
139
+ - [Helpers `encodeState` et `decodeState`](#helpers-encodestate-et-decodestate)
140
+ - [Helpers `encode` et `decode`](#helpers-encode-et-decode)
141
+ - [Bonnes pratiques](#bonnes-pratiques)
142
+ - [Pièges](#pièges)
143
+ - [Autres](#autres)
144
+ - [Contribuer et/ou exécuter localement](#contribuer-etou-exécuter-localement)
145
+ - [Feuille de route](#feuille-de-route)
146
+ - [Contact et support](#contact-et-support)
147
+ - [Journal des modifications](#journal-des-modifications)
148
+ - [Mentions](#mentions)
149
+ - [Licence](#licence)
150
+ - [Recrutez-moi](#recrutez-moi)
151
+ - [Inspiration](#inspiration)
152
+
153
+ <!-- toc:end -->
154
+
155
+ ## Installation
156
+
157
+ ### 1. Installer le paquet
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. Modifier tsconfig.json
169
+
170
+ Dans `tsconfig.json`, dans `compilerOptions`, définissez `"moduleResolution": "Bundler"`, ou `"moduleResolution": "Node16"`, ou `"moduleResolution": "NodeNext"`.
171
+ Il peut être nécessaire de définir `"module": "ES2022"`, ou `"module": "ESNext"`
172
+
173
+ ## Utilisation avec des agents de codage IA
174
+
175
+ `state-in-url` fournit des fichiers de compétences (skills) pour [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview), afin que les agents IA (Claude Code, Cursor, Copilot, Codex, etc.) chargent les bons modèles et évitent les erreurs courantes lors de l'utilisation de la bibliothèque. Après avoir installé `state-in-url`, exécutez une fois dans votre projet :
176
+
177
+ ```sh
178
+ npx @tanstack/intent@latest install
179
+ ```
180
+
181
+ Cela configure votre agent installé pour découvrir les compétences depuis `node_modules/state-in-url/skills/`. Listez les compétences disponibles avec `npx @tanstack/intent@latest list`.
182
+
183
+ ## useUrlState
184
+
185
+ Hook principal qui prend l'état initial comme paramètre et renvoie l'objet d'état, un callback pour mettre à jour l'url et un callback pour ne mettre à jour que l'état.
186
+ Tous les composants qui utilisent le même objet `state` sont automatiquement synchronisés.
187
+
188
+ ### Hook useUrlState pour Next.js
189
+
190
+ [Documentation complète de l'API](packages/urlstate/next/useUrlState)
191
+
192
+ [Exemple React-Router](#hook-useurlstate-pour-react-router)
193
+
194
+ #### Exemples d'utilisation
195
+
196
+ ##### Basique
197
+
198
+ 1. Définissez la forme de l'état avec des valeurs par défaut
199
+
200
+ ```typescript
201
+ // userState.ts
202
+ // Seuls les paramètres dont la valeur diffère de la valeur par défaut iront dans l'url.
203
+ export const userState: UserState = { name: '', age: 0 }
204
+
205
+ // Utilisez `Type`, pas `Interface` !
206
+ type UserState = { name: string, age: number }
207
+ ```
208
+
209
+ 2. Importez-le et utilisez-le
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
+ // peut passer l'argument `replace`, il contrôle si `setUrl` utilisera `router.push` ou `router.replace`, par défaut replace=true
219
+ // peut passer `searchParams` depuis des composants serveur, passez `useHistory: false` si vous devez récupérer quelque chose dans le composant serveur
220
+ const { urlState, setUrl, setState } = useUrlState(userState);
221
+
222
+ return (
223
+ <div>
224
+ // urlState.name renverra la valeur par défaut de `userState` si l'url est vide
225
+ <input value={urlState.name}
226
+ // même api que React.useState, par exemple 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
+ // Peut mettre à jour l'état immédiatement mais synchroniser le changement avec l'url selon les besoins
236
+ onBlur={() => setUrl()}
237
+ />
238
+
239
+ <button onClick={() => setUrl((_, initial) => initial)}>
240
+ Reset
241
+ </button>
242
+
243
+ </div>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ##### Avec le rendu côté serveur
249
+
250
+ <details>
251
+ <Summary>Exemple</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
+ ##### Utiliser le hook dans le composant `layout`
274
+
275
+ <details>
276
+ <Summary>Exemple</Summary>
277
+ C'est une partie délicate, car nextjs avec app router ne permet pas d'accéder à searchParams côté serveur. Il existe une solution de contournement avec un middleware, mais elle n'est pas élégante et peut cesser de fonctionner après une mise à jour de nextjs.
278
+
279
+ ```typescript
280
+ // ajoutez au `layout.tsx` approprié
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
+ // Composant layout cible
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
+ ##### Avec une forme d'état arbitraire (non recommandé)
325
+
326
+ <details>
327
+ <Summary>Exemple</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 pour Remix.js
343
+
344
+ L'API est la même que pour la version Next.js, sauf que vous pouvez passer des options du type [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
345
+
346
+ [Documentation de l'API](packages/urlstate/remix/useUrlState)
347
+
348
+ #### Exemple
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
+ // Peut mettre à jour l'état immédiatement mais synchroniser le changement avec l'url selon les besoins
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
+ [Exemple de code](packages/example-remix2/app/routes/Form-for-test.tsx)
428
+
429
+ ### Hook useUrlState pour React-Router
430
+
431
+ L'API est la même que pour la version Next.js, sauf que vous pouvez passer des options du type [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
432
+
433
+ [Documentation de l'API](packages/urlstate/react-router/useUrlState)
434
+
435
+ #### Exemple
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
+ // pour 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
+ // Peut mettre à jour l'état immédiatement mais synchroniser le changement avec l'url selon les besoins
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
+ [Exemple de code](packages/example-react-router6/src/Form-for-test.tsx)
517
+
518
+ ## Recettes
519
+ ##### Hook personnalisé pour travailler confortablement avec une tranche d'état
520
+ <details>
521
+ <Summary>Exemple</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
+ // la première navigation ajoutera une nouvelle entrée à l'historique
549
+ // toutes les suivantes ne feront que remplacer cette entrée
550
+ // ainsi l'historique n'aura que 2 entrées - ['/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
+ ##### Avec une forme d'état complexe
569
+
570
+ <details>
571
+ <Summary>Exemple</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` sera inféré du 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
+ [Exemple de code de la page de démonstration](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
646
+ </details>
647
+
648
+ ##### Mettre à jour l'état seul et le synchroniser manuellement avec l'URL
649
+
650
+ <details>
651
+ <Summary>Exemple</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
+ // comparera l'état par le contenu et non par la référence, et ne déclenchera la mise à jour que pour les nouvelles valeurs
660
+ setUrl(urlState);
661
+ }, 500);
662
+
663
+ return () => {
664
+ clearTimeout(timer.current);
665
+ };
666
+ }, [urlState, setUrl]);
667
+ ```
668
+
669
+ Synchroniser l'état sur `onBlur` sera plus conforme à l'usage réel.
670
+
671
+ ```typescript
672
+ <input onBlur={() => updateUrl()} .../>
673
+ ```
674
+
675
+ </details>
676
+
677
+ ## Autres hooks et helpers
678
+
679
+ ### Hook `useUrlStateBase` pour d'autres routeurs
680
+
681
+ Hooks pour créer vos propres hooks `useUrlState` avec d'autres routeurs, par exemple react-router ou tanstack router.
682
+
683
+ [Documentation de l'API](packages/urlstate/useUrlStateBase)
684
+
685
+ ### Hook `useSharedState` pour React.js
686
+
687
+ Hook pour partager l'état entre n'importe quels composants React, testé avec Next.js et 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
+ [Documentation de l'API](packages/urlstate/useSharedState/README.fr.md)
701
+
702
+ ### Hook `useLinkProps` pour React.js
703
+
704
+ Hook qui emporte l'état vers un lien pointant sur une autre route, par exemple un sélecteur de langue. `setUrl` écrit toujours sur le chemin courant ; celui-ci non.
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
+ Le balisage conserve le `href` tel quel : les robots d'indexation et `hreflang` voient donc l'URL canonique ; l'état est lu au clic.
722
+
723
+ [Documentation de l'API](packages/urlstate/useLinkProps/README.fr.md)
724
+
725
+ ### Hook `useUrlEncode` pour React.js
726
+
727
+ [Documentation de l'API](packages/urlstate/useUrlEncode/README.fr.md)
728
+
729
+ ### Helpers `encodeState` et `decodeState`
730
+
731
+ [Documentation de l'API](packages/urlstate/encodeState/README.fr.md)
732
+
733
+ ### Helpers `encode` et `decode`
734
+
735
+ [Documentation de l'API](packages/urlstate/encoder/README.fr.md)
736
+
737
+ ## Bonnes pratiques
738
+
739
+ - Définissez la forme de votre état comme une constante
740
+ - Utilisez TypeScript pour une meilleure sûreté de typage et l'autocomplétion
741
+ - Évitez de stocker des informations sensibles dans les paramètres d'URL (SSN, clés API, etc.)
742
+ - Utilisez cette [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) pour des erreurs TS lisibles
743
+
744
+ Vous pouvez créer des hooks d'état pour des tranches d'état et les réutiliser dans toute l'application. Par exemple :
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
+ // autre logique
761
+
762
+ // réinitialiser les paramètres de requête lors de la navigation vers une autre page
763
+ React.useEffect(() => {
764
+ return reset
765
+ }, [])
766
+
767
+ return { userState: urlState, setUserState: setUrl };;
768
+ }
769
+
770
+ ```
771
+
772
+ ## Pièges
773
+
774
+ 1. Vous ne pouvez passer que des valeurs sérialisables ; `Function`, `BigInt` ou `Symbol` ne fonctionneront pas, ni probablement des choses comme `ArrayBuffer`. Tout ce qui peut être sérialisé en JSON fonctionnera.
775
+ 2. Les serveurs Vercel limitent la taille des en-têtes (chaîne de requête et autres) à **14KB**, gardez donc l'état de votre URL sous ~5000 mots. <https://vercel.com/docs/errors/URL_TOO_LONG>
776
+ 3. Testé avec `next.js` 14/15/16 avec app router ; pas de plan pour prendre en charge pages.
777
+
778
+ ## Autres
779
+
780
+ ### Contribuer et/ou exécuter localement
781
+
782
+ Voir le [document de contribution](CONTRIBUTING.md)
783
+
784
+ ## Feuille de route
785
+
786
+ - [x] hook pour `Next.js`
787
+ - [x] hook pour `react-router`
788
+ - [x] hook pour `remix`
789
+ - [ ] hook pour `svelte`
790
+ - [ ] hook pour `astro`
791
+ - [ ] hook pour stocker l'état dans le hash ?
792
+
793
+ ## Contact et support
794
+
795
+ - Créez une [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) pour les signalements de bugs, les demandes de fonctionnalités ou les questions
796
+
797
+ ## [Journal des modifications](CHANGELOG.md)
798
+
799
+ ## Mentions
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
+ ## Licence
806
+
807
+ Ce projet est distribué sous la [licence MIT](LICENSE).
808
+
809
+ ## Recrutez-moi
810
+
811
+ Je suis **Aleksandr Smyshliaev**, unique auteur et mainteneur de cette bibliothèque.
812
+ Ingénieur frontend senior (React / Next.js / TypeScript, plus de 8 ans), et **disponible pour un travail à distance à temps plein dès maintenant**.
813
+
814
+ Cette bibliothèque est la version condensée de ce dans quoi je suis bon : une API typée au-dessus d'une primitive de navigateur désordonnée, zéro dépendance et une stabilité sur Next.js, Remix et React Router à travers plusieurs versions majeures de React.
815
+
816
+ - **Le meilleur en** — bibliothèques de composants, gestion d'état et suites de tests qui survivent à un refactor.
817
+ - **Également à moi** —
818
+ [react-horizontal-scrolling-menu](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu)
819
+ (~84k installations hebdomadaires),
820
+ [test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
821
+ (enregistrement/relecture pour Playwright),
822
+ [llm-queue](https://github.com/asmyshlyaev177/llm-queue).
823
+ - **Où** — Tbilissi, Géorgie (GMT+4), chevauchement complet avec CET. Entité contractante enregistrée, donc l'engagement B2B ne nécessite pas de configuration employer-of-record.
824
+ - **Me joindre** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
825
+ [asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
826
+ [LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
827
+
828
+ ## Inspiration
829
+
830
+ [NUQS](https://github.com/47ng/nuqs)
831
+
832
+ [Utiliser l'URL pour stocker l'état dans Vue](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
833
+
834
+ [Stocker l'état dans l'URL](https://antonz.org/storing-state/)
835
+
836
+ [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)