state-in-url 6.4.1 → 7.0.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 (55) hide show
  1. package/README.es.md +214 -7
  2. package/README.fr.md +214 -7
  3. package/README.ja.md +212 -5
  4. package/README.ko.md +212 -5
  5. package/README.md +212 -6
  6. package/README.pt-BR.md +214 -7
  7. package/README.ru.md +214 -7
  8. package/README.vi.md +214 -7
  9. package/README.zh-CN.md +214 -7
  10. package/dist/astro/index.d.ts +1 -0
  11. package/dist/astro/index.js +1 -0
  12. package/dist/astro/index.mjs +1 -0
  13. package/dist/astro/useUrlState/index.d.ts +1 -0
  14. package/dist/astro/useUrlState/index.js +1 -0
  15. package/dist/astro/useUrlState/index.mjs +1 -0
  16. package/dist/astro/useUrlState/useUrlState.d.ts +1 -0
  17. package/dist/encodeState/encodeState.d.ts +1 -1
  18. package/dist/encodeState/encodeState.js +1 -1
  19. package/dist/encodeState/encodeState.mjs +1 -1
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  24. package/dist/next/useUrlState/useUrlState.js +1 -1
  25. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  26. package/dist/package.json +68 -16
  27. package/dist/react/index.d.ts +1 -0
  28. package/dist/react/index.js +1 -0
  29. package/dist/react/index.mjs +1 -0
  30. package/dist/react/useUrlState/index.d.ts +1 -0
  31. package/dist/react/useUrlState/index.js +1 -0
  32. package/dist/react/useUrlState/index.mjs +1 -0
  33. package/dist/react/useUrlState/useUrlState.d.ts +86 -0
  34. package/dist/react/useUrlState/useUrlState.js +1 -0
  35. package/dist/react/useUrlState/useUrlState.mjs +1 -0
  36. package/dist/react-router/useUrlState/useUrlState.d.ts +5 -5
  37. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  38. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  39. package/dist/react-router6/useUrlState/useUrlState.d.ts +7 -7
  40. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  41. package/dist/remix/useUrlState/useUrlState.d.ts +5 -5
  42. package/dist/useIsomorphicLayoutEffect.js +1 -1
  43. package/dist/useIsomorphicLayoutEffect.mjs +1 -1
  44. package/dist/useLinkProps/useLinkProps.mjs +1 -1
  45. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  46. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  47. package/package.json +68 -16
  48. package/skills/astro-setup/SKILL.md +204 -0
  49. package/skills/feature-state-hook/SKILL.md +1 -1
  50. package/skills/form-library-integration/SKILL.md +1 -1
  51. package/skills/input-handling/SKILL.md +1 -1
  52. package/skills/nextjs-ssr/SKILL.md +1 -1
  53. package/skills/react-no-router/SKILL.md +215 -0
  54. package/skills/react-router-remix-setup/SKILL.md +1 -1
  55. package/skills/shared-state-no-url/SKILL.md +1 -1
package/README.es.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · Español · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
3
- <!-- i18n:meta locale=es source=README.md source-blob=bbf45a10aae9e88f68b27d84f51ee3e7e8a1e436 status=translated -->
3
+ <!-- i18n:meta locale=es source=README.md source-blob=ad78fdbbee096115a953813ed9a462e3393c5736 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@ Comparte si te resulta útil.
69
69
  Guarda cualquier estado de usuario en los parámetros de consulta; imagina JSON en la URL de un navegador. Todo ello manteniendo los tipos y la estructura de los datos: por ejemplo, los números se decodificarán como números y no como cadenas, las fechas como fechas, etc.; se admiten objetos y matrices.
70
70
  Simple a más no poder, rápido y con validación estática de TypeScript. Los enlaces profundos, es decir, la sincronización de URL, se vuelven fáciles.
71
71
 
72
- Incluye el hook `useUrlState` para Next.js y react-router, y helpers para cualquier otra cosa en JS.
72
+ Incluye el hook `useUrlState` para Next.js, react-router, Remix y Astro, y helpers para cualquier otra cosa en JS.
73
73
  Como los navegadores modernos soportan URLs enormes y a los usuarios no les importan las cadenas de consulta (es un flujo de seleccionar todo y copiar/pegar).
74
74
 
75
75
  Es hora de usar la cadena de consulta para gestionar el estado, como se pensó originalmente.
@@ -98,10 +98,32 @@ Esta biblioteca es una buena alternativa a NUQS.
98
98
  - **Renderizado en el servidor**: Se puede usar en componentes de servidor, se soportan Next.js 14, 15 y 16
99
99
  - **Ligero**: Cero dependencias, biblioteca de menos de 2KB
100
100
  - **DX**: Buena experiencia de desarrollo, documentación, comentarios JSDoc y ejemplos
101
- - **Flexibilidad de frameworks**: Hooks para `Next.js` y `react-router`, helpers para usarla con otros frameworks o JS puro
101
+ - **Flexibilidad de frameworks**: Hooks para `Next.js`, `react-router`, `Remix` y `Astro`, helpers para usarla con otros frameworks o JS puro
102
102
  - **Bien probada**: [Pruebas unitarias y pruebas de Playwright para Chrome/Firefox/Safari](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
103
  - **Licencia permisiva**: MIT
104
104
 
105
+ ### Comparación con nuqs
106
+
107
+ ¿Buscas una alternativa a [nuqs](https://github.com/47ng/nuqs)? Ambas guardan estado tipado en la query string; difieren en cuánto hay que configurar y en qué puede ser un valor.
108
+
109
+ | Qué | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | Configuración | Ninguna — importa el hook y listo | Un componente adaptador envuelve la app |
112
+ | Forma del estado | Un objeto tipado, como `React.useState` | Valores por clave, con un parser declarado para cada una |
113
+ | Reutilización entre componentes | Envuelve el hook una vez — cada componente comparte el estado, sin props | Extraes tu propio hook alrededor del mapa de parsers |
114
+ | Objetos y arrays anidados | Integrado — estructura y tipos se conservan | Parser JSON más tu propio validador |
115
+ | Fechas | Se conservan automáticamente | Parser integrado, declarado por clave |
116
+ | Tamaño, import completo | ~2,9 KB gzip | ~6,7 KB gzip |
117
+ | Dependencias en runtime | Ninguna | Una |
118
+ | Routers | Next.js, React Router v6/v7, Remix, Astro, helpers para JS puro | Next.js, React Router, Remix, TanStack Router, React puro |
119
+
120
+ Tamaños: import de toda la librería, esbuild minify + gzip, medido en agosto de 2026 contra nuqs 2.10.1.
121
+
122
+ nuqs es una buena librería: elígela si quieres cada valor como su propio query param legible o usas TanStack Router. Elige state-in-url cuando quieras un objeto tipado completo en la URL sin configuración.
123
+
124
+
125
+ La comparación completa — la misma feature en ambas, otras alternativas (TanStack Router, use-query-params) y notas de migración — está en <https://state-in-url.dev/vs/nuqs>.
126
+
105
127
  ## Tabla de contenido
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Esta biblioteca es una buena alternativa a NUQS.
111
133
  - [¿Por qué usar `state-in-url`?](#por-qué-usar-state-in-url)
112
134
  - [Casos de uso](#casos-de-uso)
113
135
  - [Características](#características)
136
+ - [Comparación con nuqs](#comparación-con-nuqs)
114
137
  - [Tabla de contenido](#tabla-de-contenido)
115
138
  - [Instalación](#instalación)
116
139
  - [1. Instalar el paquete](#1-instalar-el-paquete)
@@ -127,12 +150,16 @@ Esta biblioteca es una buena alternativa a NUQS.
127
150
  - [Ejemplo](#ejemplo)
128
151
  - [Hook useUrlState para React-Router](#hook-useurlstate-para-react-router)
129
152
  - [Ejemplo](#ejemplo-1)
153
+ - [Hook useUrlState para React (sin router)](#hook-useurlstate-para-react-sin-router)
154
+ - [Ejemplo](#ejemplo-2)
155
+ - [Hook useUrlState para Astro](#hook-useurlstate-para-astro)
156
+ - [Ejemplo](#ejemplo-3)
130
157
  - [Recetas](#recetas)
131
158
  - [Hook personalizado para trabajar cómodamente con una porción del estado](#hook-personalizado-para-trabajar-cómodamente-con-una-porción-del-estado)
132
159
  - [Con una forma de estado compleja](#con-una-forma-de-estado-compleja)
133
160
  - [Actualizar solo el estado y sincronizarlo manualmente con la URL](#actualizar-solo-el-estado-y-sincronizarlo-manualmente-con-la-url)
134
161
  - [Otros hooks y helpers](#otros-hooks-y-helpers)
135
- - [Hook `useUrlStateBase` para otros routers](#hook-useurlstatebase-para-otros-routers)
162
+ - [Hook `useUrlStateBase` para otro router](#hook-useurlstatebase-para-otro-router)
136
163
  - [Hook `useSharedState` para React.js](#hook-usesharedstate-para-reactjs)
137
164
  - [Hook `useLinkProps` para React.js](#hook-uselinkprops-para-reactjs)
138
165
  - [Hook `useUrlEncode` para React.js](#hook-useurlencode-para-reactjs)
@@ -515,6 +542,156 @@ const tags = [
515
542
 
516
543
  [Ejemplo de código](packages/example-react-router6/src/Form-for-test.tsx)
517
544
 
545
+ ### Hook useUrlState para React (sin router)
546
+
547
+ Para una aplicación React sin router: Vite, Create React App, o un widget que se monta en una página que no controlas. El hook escribe la URL con `window.history` y la vuelve a leer al navegar atrás y adelante y ante cualquier otro `pushState`/`replaceState`. Todos los componentes que usan el mismo objeto de estado lo comparten, sin nada que envolver y sin nada que pasar por props.
548
+
549
+ [Documentación de la API](packages/urlstate/react/useUrlState)
550
+
551
+ #### Ejemplo
552
+
553
+ ```typescript
554
+ // useFilters.ts
555
+ import { useUrlState } from 'state-in-url/react';
556
+
557
+ type FiltersState = { sort: 'name' | 'date'; page: number };
558
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
559
+
560
+ export function useFilters() {
561
+ return useUrlState(FILTERS_STATE);
562
+ }
563
+ ```
564
+
565
+ ```typescript
566
+ // FiltersBar.tsx
567
+ import { useFilters } from './useFilters';
568
+
569
+ export function FiltersBar() {
570
+ const { urlState, setUrl } = useFilters();
571
+
572
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
573
+ }
574
+ ```
575
+
576
+ `state-in-url/astro` es este mismo hook; se documenta aparte más abajo porque las islands además reciben `searchParams` como prop.
577
+
578
+ ### Hook useUrlState para Astro
579
+
580
+ Para islas de React. Astro no tiene router del lado del cliente por defecto, así que el hook escribe la URL con `window.history` y la vuelve a leer al navegar atrás/adelante y en cualquier otro `pushState`/`replaceState`, incluido el propio `<ClientRouter />` de Astro. Las islas de una página comparten el estado, sin nada que las envuelva.
581
+
582
+ [Documentación de la API](packages/urlstate/astro/useUrlState)
583
+
584
+ #### Ejemplo
585
+
586
+ ```typescript
587
+ // src/state.ts
588
+ export const form: Form = {
589
+ name: '',
590
+ age: undefined,
591
+ agree_to_terms: false,
592
+ tags: [],
593
+ };
594
+
595
+ type Form = {
596
+ name: string;
597
+ age?: number;
598
+ agree_to_terms: boolean;
599
+ tags: { id: string; value: { text: string; time: Date } }[];
600
+ };
601
+ ```
602
+
603
+ La página debe renderizarse bajo demanda (`output: 'server'`, o `export const prerender = false` en la página, con un adaptador): una página prerenderizada no tiene petición, así que la isla recibe `{}` y lee la URL después de la hidratación.
604
+
605
+ ```astro
606
+ ---
607
+ // src/pages/index.astro
608
+ import { Form } from '../components/Form';
609
+ import { Status } from '../components/Status';
610
+
611
+ // El renderizado en el servidor coincide con la URL, así que la hidratación no tiene nada que corregir.
612
+ // Un objeto plano: las props de las islas se serializan, URLSearchParams no.
613
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
614
+ ---
615
+
616
+ <Form client:load searchParams={searchParams} />
617
+ <Status client:load searchParams={searchParams} />
618
+ ```
619
+
620
+ ```typescript
621
+ // src/components/Form.tsx
622
+ import React from 'react';
623
+ import { useUrlState } from 'state-in-url/astro';
624
+
625
+ import { form } from '../state';
626
+
627
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
628
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
629
+
630
+ const onChangeTags = React.useCallback(
631
+ (tag: (typeof tags)[number]) => {
632
+ setUrl((curr) => ({
633
+ ...curr,
634
+ tags: curr.tags.find((t) => t.id === tag.id)
635
+ ? curr.tags.filter((t) => t.id !== tag.id)
636
+ : curr.tags.concat(tag),
637
+ }));
638
+ },
639
+ [setUrl],
640
+ );
641
+
642
+ return (
643
+ <div>
644
+ {tags.map((tag) => (
645
+ <Tag
646
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
647
+ text={tag.value.text}
648
+ onClick={() => onChangeTags(tag)}
649
+ key={tag.id}
650
+ />
651
+ ))}
652
+
653
+ <input value={urlState.name}
654
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
655
+ // Puede actualizar el estado inmediatamente pero sincronizar el cambio con la url según sea necesario
656
+ onBlur={() => setUrl()}
657
+ />
658
+ </div>
659
+ );
660
+ }
661
+
662
+ const tags = [
663
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
664
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
665
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
666
+ ];
667
+
668
+ // Status.tsx, una segunda isla, lee el mismo estado
669
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
670
+ const { urlState } = useUrlState(form, { searchParams });
671
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
672
+ }
673
+ ```
674
+
675
+ Las islas de Preact funcionan igual: con `@astrojs/preact` y `compat: true`, `react` se resuelve a `preact/compat` tanto en la compilación del servidor como en la del cliente, y el import de arriba no cambia.
676
+
677
+ Sin islas, en una página sin ningún framework de cliente, el mismo estado vive en el frontmatter a través de [`decodeState` y `encodeState`](#helpers-encodestate-y-decodestate):
678
+
679
+ ```astro
680
+ ---
681
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
682
+
683
+ import { form } from '../state';
684
+
685
+ const state = decodeState(Astro.url.searchParams, form);
686
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
687
+ ---
688
+
689
+ <pre>{JSON.stringify(state)}</pre>
690
+ <a href={`?${withName}`}>Alice</a>
691
+ ```
692
+
693
+ [Ejemplo de código](packages/example-astro/src/components/Form-for-test.tsx), [página en Astro puro](packages/example-astro/src/pages/pure-astro.astro)
694
+
518
695
  ## Recetas
519
696
  ##### Hook personalizado para trabajar cómodamente con una porción del estado
520
697
  <details>
@@ -676,9 +853,39 @@ Sincronizar el estado en `onBlur` estará más alineado con el uso en el mundo r
676
853
 
677
854
  ## Otros hooks y helpers
678
855
 
679
- ### Hook `useUrlStateBase` para otros routers
856
+ ### Hook `useUrlStateBase` para otro router
857
+
858
+ Úsalo para construir tu propio `useUrlState` para un router del que este paquete no publica un punto de entrada, por ejemplo TanStack Router. Pasa cualquier objeto memoizado con `push(url)` y `replace(url)`.
859
+
860
+ Si **no hay router alguno** no lo necesitas: usa [`state-in-url/react`](#hook-useurlstate-para-react-sin-router), que es este mismo hook ya conectado a `window.history`.
861
+
862
+ ```typescript
863
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
864
+
865
+ type FiltersState = { sort: 'name' | 'date'; page: number };
866
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
867
+
868
+ export function useFilters() {
869
+ const router = useYourRouter();
870
+ const nav = React.useMemo(
871
+ () => ({
872
+ push: (url: string) => router.navigate(url),
873
+ replace: (url: string) => router.navigate(url, { replace: true }),
874
+ }),
875
+ [router],
876
+ );
877
+
878
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
879
+ }
880
+ ```
881
+
882
+ Dos diferencias respecto a `useUrlState`:
883
+
884
+ - Devuelve `state`, `updateUrl` y `updateState`, no `urlState`, `setUrl` ni `setState`; además `updateUrl` hace push por defecto, mientras que `setUrl` hace replace.
885
+ - El tercer argumento define el state inicial. Si lo omites, el hook ignora la cadena de consulta con la que se cargó la página, así que un enlace compartido se abre con los valores por defecto.
680
886
 
681
- Hooks para crear tus propios hooks `useUrlState` con otros routers, p. ej. react-router o tanstack router.
887
+ > [!NOTE]
888
+ > `useUrlState` ya no se exporta desde la raíz del paquete: cada uno está atado a un framework, así que cada uno vive en su propio punto de entrada (`state-in-url/next`, `/react`, `/react-router`, `/react-router6`, `/remix`, `/astro`). La raíz solo conserva lo que es independiente del framework: `useSharedState`, `useUrlEncode`, `useUrlStateBase`, `useLinkProps`, `encode`/`decode` y `encodeState`/`decodeState`.
682
889
 
683
890
  [Documentación de la API](packages/urlstate/useUrlStateBase)
684
891
 
@@ -787,7 +994,7 @@ Consulta el [documento de contribución](CONTRIBUTING.md)
787
994
  - [x] hook para `react-router`
788
995
  - [x] hook para `remix`
789
996
  - [ ] hook para `svelte`
790
- - [ ] hook para `astro`
997
+ - [x] hook para `astro`
791
998
  - [ ] hook para guardar el estado en el hash ?
792
999
 
793
1000
  ## Contacto y soporte
package/README.fr.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
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 -->
3
+ <!-- i18n:meta locale=fr source=README.md source-blob=ad78fdbbee096115a953813ed9a462e3393c5736 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@ Partagez si cela vous est utile.
69
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
70
  Extrêmement simple, rapide et avec une validation TypeScript statique. Les liens profonds, autrement dit la synchronisation d'URL, deviennent faciles.
71
71
 
72
- Contient le hook `useUrlState` pour Next.js et react-router, ainsi que des helpers pour tout le reste en JS.
72
+ Contient le hook `useUrlState` pour Next.js, react-router, Remix et Astro, ainsi que des helpers pour tout le reste en JS.
73
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
74
 
75
75
  Il est temps d'utiliser la chaîne de requête pour la gestion d'état, comme cela était prévu à l'origine.
@@ -98,10 +98,32 @@ Cette bibliothèque est une bonne alternative à NUQS.
98
98
  - **Rendu côté serveur** : Utilisable dans les composants serveur, Next.js 14, 15 et 16 sont pris en charge
99
99
  - **Léger** : Zéro dépendance, bibliothèque de moins de 2KB
100
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
101
+ - **Flexibilité de frameworks** : Hooks pour `Next.js`, `react-router`, `Remix` et `Astro`, helpers pour l'utiliser avec d'autres frameworks ou du JS pur
102
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
103
  - **Licence permissive** : MIT
104
104
 
105
+ ### Comparaison avec nuqs
106
+
107
+ Vous cherchez une alternative à [nuqs](https://github.com/47ng/nuqs) ? Les deux gardent un état typé dans la query string ; elles diffèrent par la configuration requise et par ce qu’une valeur peut être.
108
+
109
+ | Quoi | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | Mise en place | Aucune — importez le hook et c’est parti | Un composant adaptateur enveloppe l’application |
112
+ | Forme de l’état | Un objet typé, comme `React.useState` | Valeurs par clé, un parseur déclaré pour chacune |
113
+ | Réutilisation entre composants | Enveloppez le hook une fois — chaque composant partage l’état, sans props | Vous extrayez votre propre hook autour de la table de parseurs |
114
+ | Objets et tableaux imbriqués | Intégré — structure et types préservés | Parseur JSON plus votre propre validateur |
115
+ | Dates | Préservées automatiquement | Parseur intégré, déclaré par clé |
116
+ | Taille, import complet | ~2,9 Ko gzip | ~6,7 Ko gzip |
117
+ | Dépendances au runtime | Aucune | Une |
118
+ | Routeurs | Next.js, React Router v6/v7, Remix, Astro, helpers pour JS pur | Next.js, React Router, Remix, TanStack Router, React pur |
119
+
120
+ Tailles : import de la bibliothèque entière, esbuild minify + gzip, mesuré en août 2026 face à nuqs 2.10.1.
121
+
122
+ nuqs est une bonne bibliothèque — choisissez-la pour un query param lisible par valeur, ou si vous êtes sur TanStack Router. Choisissez state-in-url pour un objet typé complet dans l’URL, sans configuration.
123
+
124
+
125
+ La comparaison complète — la même fonctionnalité dans les deux, d’autres alternatives (TanStack Router, use-query-params) et des notes de migration — se trouve sur <https://state-in-url.dev/vs/nuqs>.
126
+
105
127
  ## Table des matières
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Cette bibliothèque est une bonne alternative à NUQS.
111
133
  - [Pourquoi utiliser `state-in-url` ?](#pourquoi-utiliser-state-in-url-)
112
134
  - [Cas d'utilisation](#cas-dutilisation)
113
135
  - [Fonctionnalités](#fonctionnalités)
136
+ - [Comparaison avec nuqs](#comparaison-avec-nuqs)
114
137
  - [Table des matières](#table-des-matières)
115
138
  - [Installation](#installation)
116
139
  - [1. Installer le paquet](#1-installer-le-paquet)
@@ -127,12 +150,16 @@ Cette bibliothèque est une bonne alternative à NUQS.
127
150
  - [Exemple](#exemple)
128
151
  - [Hook useUrlState pour React-Router](#hook-useurlstate-pour-react-router)
129
152
  - [Exemple](#exemple-1)
153
+ - [Hook useUrlState pour React (sans routeur)](#hook-useurlstate-pour-react-sans-routeur)
154
+ - [Exemple](#exemple-2)
155
+ - [Hook useUrlState pour Astro](#hook-useurlstate-pour-astro)
156
+ - [Exemple](#exemple-3)
130
157
  - [Recettes](#recettes)
131
158
  - [Hook personnalisé pour travailler confortablement avec une tranche d'état](#hook-personnalisé-pour-travailler-confortablement-avec-une-tranche-détat)
132
159
  - [Avec une forme d'état complexe](#avec-une-forme-détat-complexe)
133
160
  - [Mettre à jour l'état seul et le synchroniser manuellement avec l'URL](#mettre-à-jour-létat-seul-et-le-synchroniser-manuellement-avec-lurl)
134
161
  - [Autres hooks et helpers](#autres-hooks-et-helpers)
135
- - [Hook `useUrlStateBase` pour d'autres routeurs](#hook-useurlstatebase-pour-dautres-routeurs)
162
+ - [Hook `useUrlStateBase` pour un autre routeur](#hook-useurlstatebase-pour-un-autre-routeur)
136
163
  - [Hook `useSharedState` pour React.js](#hook-usesharedstate-pour-reactjs)
137
164
  - [Hook `useLinkProps` pour React.js](#hook-uselinkprops-pour-reactjs)
138
165
  - [Hook `useUrlEncode` pour React.js](#hook-useurlencode-pour-reactjs)
@@ -515,6 +542,156 @@ const tags = [
515
542
 
516
543
  [Exemple de code](packages/example-react-router6/src/Form-for-test.tsx)
517
544
 
545
+ ### Hook useUrlState pour React (sans routeur)
546
+
547
+ Pour une application React sans routeur : Vite, Create React App, ou un widget monté dans une page que vous ne contrôlez pas. Le hook écrit l'URL via `window.history` et la relit lors des retours et avances dans l'historique ainsi qu'à chaque autre `pushState`/`replaceState`. Tous les composants qui utilisent le même objet d'état le partagent, sans rien à englober ni rien à faire transiter par les props.
548
+
549
+ [Documentation de l'API](packages/urlstate/react/useUrlState)
550
+
551
+ #### Exemple
552
+
553
+ ```typescript
554
+ // useFilters.ts
555
+ import { useUrlState } from 'state-in-url/react';
556
+
557
+ type FiltersState = { sort: 'name' | 'date'; page: number };
558
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
559
+
560
+ export function useFilters() {
561
+ return useUrlState(FILTERS_STATE);
562
+ }
563
+ ```
564
+
565
+ ```typescript
566
+ // FiltersBar.tsx
567
+ import { useFilters } from './useFilters';
568
+
569
+ export function FiltersBar() {
570
+ const { urlState, setUrl } = useFilters();
571
+
572
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
573
+ }
574
+ ```
575
+
576
+ `state-in-url/astro` est ce même hook ; il est documenté séparément plus bas parce que les îlots reçoivent en plus `searchParams` sous forme de prop.
577
+
578
+ ### Hook useUrlState pour Astro
579
+
580
+ Pour les îlots React. Astro n'a pas de routeur côté client par défaut, donc le hook écrit l'URL avec `window.history` et la relit lors des navigations précédent/suivant et lors de tout autre `pushState`/`replaceState`, y compris le `<ClientRouter />` d'Astro. Les îlots d'une page partagent l'état, sans avoir à les envelopper dans quoi que ce soit.
581
+
582
+ [Documentation de l'API](packages/urlstate/astro/useUrlState)
583
+
584
+ #### Exemple
585
+
586
+ ```typescript
587
+ // src/state.ts
588
+ export const form: Form = {
589
+ name: '',
590
+ age: undefined,
591
+ agree_to_terms: false,
592
+ tags: [],
593
+ };
594
+
595
+ type Form = {
596
+ name: string;
597
+ age?: number;
598
+ agree_to_terms: boolean;
599
+ tags: { id: string; value: { text: string; time: Date } }[];
600
+ };
601
+ ```
602
+
603
+ La page doit être rendue à la demande (`output: 'server'`, ou `export const prerender = false` sur la page, avec un adaptateur) : une page prérendue n'a pas de requête, donc l'îlot reçoit `{}` et lit l'URL après l'hydratation.
604
+
605
+ ```astro
606
+ ---
607
+ // src/pages/index.astro
608
+ import { Form } from '../components/Form';
609
+ import { Status } from '../components/Status';
610
+
611
+ // Le rendu serveur correspond à l'URL, donc l'hydratation n'a rien à corriger.
612
+ // Un objet simple : les props d'îlot sont sérialisées, URLSearchParams ne l'est pas.
613
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
614
+ ---
615
+
616
+ <Form client:load searchParams={searchParams} />
617
+ <Status client:load searchParams={searchParams} />
618
+ ```
619
+
620
+ ```typescript
621
+ // src/components/Form.tsx
622
+ import React from 'react';
623
+ import { useUrlState } from 'state-in-url/astro';
624
+
625
+ import { form } from '../state';
626
+
627
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
628
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
629
+
630
+ const onChangeTags = React.useCallback(
631
+ (tag: (typeof tags)[number]) => {
632
+ setUrl((curr) => ({
633
+ ...curr,
634
+ tags: curr.tags.find((t) => t.id === tag.id)
635
+ ? curr.tags.filter((t) => t.id !== tag.id)
636
+ : curr.tags.concat(tag),
637
+ }));
638
+ },
639
+ [setUrl],
640
+ );
641
+
642
+ return (
643
+ <div>
644
+ {tags.map((tag) => (
645
+ <Tag
646
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
647
+ text={tag.value.text}
648
+ onClick={() => onChangeTags(tag)}
649
+ key={tag.id}
650
+ />
651
+ ))}
652
+
653
+ <input value={urlState.name}
654
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
655
+ // Peut mettre à jour l'état immédiatement mais synchroniser le changement avec l'url selon les besoins
656
+ onBlur={() => setUrl()}
657
+ />
658
+ </div>
659
+ );
660
+ }
661
+
662
+ const tags = [
663
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
664
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
665
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
666
+ ];
667
+
668
+ // Status.tsx, un second îlot, lit le même état
669
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
670
+ const { urlState } = useUrlState(form, { searchParams });
671
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
672
+ }
673
+ ```
674
+
675
+ Les îlots Preact fonctionnent de la même manière : avec `@astrojs/preact` et `compat: true`, `react` est résolu vers `preact/compat` aussi bien dans le build serveur que dans le build client, et l'import ci-dessus reste inchangé.
676
+
677
+ Sans îlots, sur une page sans aucun framework client, le même état vit dans le frontmatter via [`decodeState` et `encodeState`](#helpers-encodestate-et-decodestate) :
678
+
679
+ ```astro
680
+ ---
681
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
682
+
683
+ import { form } from '../state';
684
+
685
+ const state = decodeState(Astro.url.searchParams, form);
686
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
687
+ ---
688
+
689
+ <pre>{JSON.stringify(state)}</pre>
690
+ <a href={`?${withName}`}>Alice</a>
691
+ ```
692
+
693
+ [Exemple de code](packages/example-astro/src/components/Form-for-test.tsx), [page Astro pure](packages/example-astro/src/pages/pure-astro.astro)
694
+
518
695
  ## Recettes
519
696
  ##### Hook personnalisé pour travailler confortablement avec une tranche d'état
520
697
  <details>
@@ -676,9 +853,39 @@ Synchroniser l'état sur `onBlur` sera plus conforme à l'usage réel.
676
853
 
677
854
  ## Autres hooks et helpers
678
855
 
679
- ### Hook `useUrlStateBase` pour d'autres routeurs
856
+ ### Hook `useUrlStateBase` pour un autre routeur
857
+
858
+ Utilisez-le pour construire votre propre `useUrlState` destiné à un routeur pour lequel ce paquet ne publie pas de point d'entrée, par exemple TanStack Router. Passez n'importe quel objet mémoïsé possédant `push(url)` et `replace(url)`.
859
+
860
+ S'il n'y a **aucun routeur**, vous n'en avez pas besoin : utilisez [`state-in-url/react`](#hook-useurlstate-pour-react-sans-routeur), qui est ce même hook déjà relié à `window.history`.
861
+
862
+ ```typescript
863
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
864
+
865
+ type FiltersState = { sort: 'name' | 'date'; page: number };
866
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
867
+
868
+ export function useFilters() {
869
+ const router = useYourRouter();
870
+ const nav = React.useMemo(
871
+ () => ({
872
+ push: (url: string) => router.navigate(url),
873
+ replace: (url: string) => router.navigate(url, { replace: true }),
874
+ }),
875
+ [router],
876
+ );
877
+
878
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
879
+ }
880
+ ```
881
+
882
+ Deux différences avec `useUrlState` :
883
+
884
+ - Il renvoie `state`, `updateUrl`, `updateState`, et non `urlState`, `setUrl`, `setState` ; de plus `updateUrl` fait un push par défaut, là où `setUrl` fait un replace.
885
+ - Le troisième argument fournit le state initial. Si vous l'omettez, le hook ignore la chaîne de requête avec laquelle la page a été chargée, et un lien partagé s'ouvre donc sur les valeurs par défaut.
680
886
 
681
- Hooks pour créer vos propres hooks `useUrlState` avec d'autres routeurs, par exemple react-router ou tanstack router.
887
+ > [!NOTE]
888
+ > `useUrlState` n'est plus exporté depuis la racine du paquet : chacun est lié à un framework, et chacun vit donc derrière son propre point d'entrée (`state-in-url/next`, `/react`, `/react-router`, `/react-router6`, `/remix`, `/astro`). La racine ne conserve que ce qui est indépendant du framework : `useSharedState`, `useUrlEncode`, `useUrlStateBase`, `useLinkProps`, `encode`/`decode` et `encodeState`/`decodeState`.
682
889
 
683
890
  [Documentation de l'API](packages/urlstate/useUrlStateBase)
684
891
 
@@ -787,7 +994,7 @@ Voir le [document de contribution](CONTRIBUTING.md)
787
994
  - [x] hook pour `react-router`
788
995
  - [x] hook pour `remix`
789
996
  - [ ] hook pour `svelte`
790
- - [ ] hook pour `astro`
997
+ - [x] hook pour `astro`
791
998
  - [ ] hook pour stocker l'état dans le hash ?
792
999
 
793
1000
  ## Contact et support