state-in-url 6.5.0 → 8.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 (100) hide show
  1. package/README.es.md +120 -5
  2. package/README.fr.md +120 -5
  3. package/README.ja.md +117 -3
  4. package/README.ko.md +118 -5
  5. package/README.md +116 -4
  6. package/README.pt-BR.md +120 -5
  7. package/README.ru.md +120 -5
  8. package/README.vi.md +120 -5
  9. package/README.zh-CN.md +119 -7
  10. package/dist/astro/index.cjs +1 -0
  11. package/dist/astro/index.mjs +1 -1
  12. package/dist/astro/useUrlState/index.cjs +1 -0
  13. package/dist/astro/useUrlState/index.mjs +1 -1
  14. package/dist/astro/useUrlState/useUrlState.d.ts +1 -85
  15. package/dist/constants/constants.cjs +1 -0
  16. package/dist/constants/index.cjs +1 -0
  17. package/dist/encodeState/encodeState.cjs +1 -0
  18. package/dist/encodeState/index.cjs +1 -0
  19. package/dist/encoder/encoder.cjs +1 -0
  20. package/dist/encoder/index.cjs +1 -0
  21. package/dist/index.cjs +1 -0
  22. package/dist/index.d.ts +1 -2
  23. package/dist/index.mjs +1 -1
  24. package/dist/next/index.cjs +1 -0
  25. package/dist/next/useUrlState/index.cjs +1 -0
  26. package/dist/next/useUrlState/useUrlState.cjs +1 -0
  27. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  28. package/dist/package.json +48 -23
  29. package/dist/parseSPObj.cjs +1 -0
  30. package/dist/react/index.cjs +1 -0
  31. package/dist/react/index.d.ts +1 -0
  32. package/dist/react/index.mjs +1 -0
  33. package/dist/react/useUrlState/index.cjs +1 -0
  34. package/dist/react/useUrlState/index.d.ts +1 -0
  35. package/dist/react/useUrlState/index.mjs +1 -0
  36. package/dist/react/useUrlState/useUrlState.cjs +1 -0
  37. package/dist/react/useUrlState/useUrlState.d.ts +86 -0
  38. package/dist/react-router/index.cjs +1 -0
  39. package/dist/react-router/useUrlState/index.cjs +1 -0
  40. package/dist/react-router/useUrlState/useUrlState.cjs +1 -0
  41. package/dist/react-router6/index.cjs +1 -0
  42. package/dist/react-router6/useUrlState/index.cjs +1 -0
  43. package/dist/react-router6/useUrlState/useUrlState.cjs +1 -0
  44. package/dist/remix/index.cjs +1 -0
  45. package/dist/remix/useUrlState/index.cjs +1 -0
  46. package/dist/remix/useUrlState/useUrlState.cjs +1 -0
  47. package/dist/useLinkProps/hrefWithState.cjs +1 -0
  48. package/dist/useLinkProps/index.cjs +1 -0
  49. package/dist/useLinkProps/useLinkProps.cjs +1 -0
  50. package/dist/useSharedState/index.cjs +1 -0
  51. package/dist/useSharedState/useSharedState.cjs +1 -0
  52. package/dist/useUrlEncode/index.cjs +1 -0
  53. package/dist/useUrlEncode/useUrlEncode.cjs +1 -0
  54. package/dist/useUrlStateBase/index.cjs +1 -0
  55. package/dist/useUrlStateBase/useUrlStateBase.cjs +1 -0
  56. package/package.json +48 -23
  57. package/skills/astro-setup/SKILL.md +1 -1
  58. package/skills/feature-state-hook/SKILL.md +1 -1
  59. package/skills/form-library-integration/SKILL.md +1 -1
  60. package/skills/input-handling/SKILL.md +1 -1
  61. package/skills/nextjs-ssr/SKILL.md +1 -1
  62. package/skills/react-no-router/SKILL.md +215 -0
  63. package/skills/react-router-remix-setup/SKILL.md +55 -1
  64. package/skills/shared-state-no-url/SKILL.md +1 -1
  65. package/dist/astro/index.js +0 -1
  66. package/dist/astro/useUrlState/index.js +0 -1
  67. package/dist/astro/useUrlState/useUrlState.js +0 -1
  68. package/dist/constants/constants.js +0 -1
  69. package/dist/constants/index.js +0 -1
  70. package/dist/encodeState/encodeState.js +0 -1
  71. package/dist/encodeState/index.js +0 -1
  72. package/dist/encoder/encoder.js +0 -1
  73. package/dist/encoder/index.js +0 -1
  74. package/dist/index.js +0 -1
  75. package/dist/next/index.js +0 -1
  76. package/dist/next/useUrlState/index.js +0 -1
  77. package/dist/next/useUrlState/useUrlState.js +0 -1
  78. package/dist/parseSPObj.js +0 -1
  79. package/dist/react-router/index.js +0 -1
  80. package/dist/react-router/useUrlState/index.js +0 -1
  81. package/dist/react-router/useUrlState/useUrlState.js +0 -1
  82. package/dist/react-router6/index.js +0 -1
  83. package/dist/react-router6/useUrlState/index.js +0 -1
  84. package/dist/react-router6/useUrlState/useUrlState.js +0 -1
  85. package/dist/remix/index.js +0 -1
  86. package/dist/remix/useUrlState/index.js +0 -1
  87. package/dist/remix/useUrlState/useUrlState.js +0 -1
  88. package/dist/useLinkProps/hrefWithState.js +0 -1
  89. package/dist/useLinkProps/index.js +0 -1
  90. package/dist/useLinkProps/useLinkProps.js +0 -1
  91. package/dist/useSharedState/index.js +0 -1
  92. package/dist/useSharedState/useSharedState.js +0 -1
  93. package/dist/useUrlEncode/index.js +0 -1
  94. package/dist/useUrlEncode/useUrlEncode.js +0 -1
  95. package/dist/useUrlStateBase/index.js +0 -1
  96. package/dist/useUrlStateBase/useUrlStateBase.js +0 -1
  97. /package/dist/{astro → react}/useUrlState/useUrlState.mjs +0 -0
  98. /package/dist/{subscribers.js → subscribers.cjs} +0 -0
  99. /package/dist/{useIsomorphicLayoutEffect.js → useIsomorphicLayoutEffect.cjs} +0 -0
  100. /package/dist/{utils.js → utils.cjs} +0 -0
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=4d98ba970e6aab8f29987fbcba29fb6c61c2fc67 status=translated -->
3
+ <!-- i18n:meta locale=es source=README.md source-blob=8b11ee8f8c8b83c207a940455766e24376508130 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -138,6 +138,7 @@ La comparación completa — la misma feature en ambas, otras alternativas (TanS
138
138
  - [Instalación](#instalación)
139
139
  - [1. Instalar el paquete](#1-instalar-el-paquete)
140
140
  - [2. Editar tsconfig.json](#2-editar-tsconfigjson)
141
+ - [3. Nota: este paquete se resuelve solo como ESM](#3-nota-este-paquete-se-resuelve-solo-como-esm)
141
142
  - [Uso con agentes de codificación con IA](#uso-con-agentes-de-codificación-con-ia)
142
143
  - [useUrlState](#useurlstate)
143
144
  - [Hook useUrlState para Next.js](#hook-useurlstate-para-nextjs)
@@ -150,14 +151,16 @@ La comparación completa — la misma feature en ambas, otras alternativas (TanS
150
151
  - [Ejemplo](#ejemplo)
151
152
  - [Hook useUrlState para React-Router](#hook-useurlstate-para-react-router)
152
153
  - [Ejemplo](#ejemplo-1)
153
- - [Hook useUrlState para Astro](#hook-useurlstate-para-astro)
154
+ - [Hook useUrlState para React (sin router)](#hook-useurlstate-para-react-sin-router)
154
155
  - [Ejemplo](#ejemplo-2)
156
+ - [Hook useUrlState para Astro](#hook-useurlstate-para-astro)
157
+ - [Ejemplo](#ejemplo-3)
155
158
  - [Recetas](#recetas)
156
159
  - [Hook personalizado para trabajar cómodamente con una porción del estado](#hook-personalizado-para-trabajar-cómodamente-con-una-porción-del-estado)
157
160
  - [Con una forma de estado compleja](#con-una-forma-de-estado-compleja)
158
161
  - [Actualizar solo el estado y sincronizarlo manualmente con la URL](#actualizar-solo-el-estado-y-sincronizarlo-manualmente-con-la-url)
159
162
  - [Otros hooks y helpers](#otros-hooks-y-helpers)
160
- - [Hook `useUrlStateBase` para otros routers](#hook-useurlstatebase-para-otros-routers)
163
+ - [Hook `useUrlStateBase` para otro router](#hook-useurlstatebase-para-otro-router)
161
164
  - [Hook `useSharedState` para React.js](#hook-usesharedstate-para-reactjs)
162
165
  - [Hook `useLinkProps` para React.js](#hook-uselinkprops-para-reactjs)
163
166
  - [Hook `useUrlEncode` para React.js](#hook-useurlencode-para-reactjs)
@@ -195,6 +198,20 @@ pnpm add state-in-url
195
198
  En `tsconfig.json`, en `compilerOptions`, establece `"moduleResolution": "Bundler"`, o `"moduleResolution": "Node16"`, o `"moduleResolution": "NodeNext"`.
196
199
  Posiblemente necesites establecer `"module": "ES2022"`, o `"module": "ESNext"`
197
200
 
201
+ ### 3. Nota: este paquete se resuelve solo como ESM
202
+
203
+ Next.js, Vite, Astro y Remix lo empaquetan sin ninguna configuración, y
204
+ `require()` desde Node puro funciona en Node `^20.19` o `>=22.12`, donde
205
+ `require(esm)` no necesita flag.
206
+
207
+ En `dist/` también se publica una compilación CommonJS con extensión `.cjs`, pero
208
+ el mapa `exports` no apunta a ella, así que ninguna resolución llega allí por
209
+ accidente. Está para los casos límite — Node antiguo, o Jest sin flag — y solo se
210
+ alcanza mediante una ruta explícita.
211
+
212
+ **En cualquiera de los dos casos Jest necesita una línea de configuración** —
213
+ consulta el punto 5 de [Trampas](#trampas).
214
+
198
215
  ## Uso con agentes de codificación con IA
199
216
 
200
217
  `state-in-url` incluye archivos de habilidades (skills) para [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview), de modo que los agentes de IA (Claude Code, Cursor, Copilot, Codex, etc.) carguen los patrones correctos y eviten errores comunes al usar la biblioteca. Tras instalar `state-in-url`, ejecuta una vez en tu proyecto:
@@ -540,6 +557,39 @@ const tags = [
540
557
 
541
558
  [Ejemplo de código](packages/example-react-router6/src/Form-for-test.tsx)
542
559
 
560
+ ### Hook useUrlState para React (sin router)
561
+
562
+ 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.
563
+
564
+ [Documentación de la API](packages/urlstate/react/useUrlState)
565
+
566
+ #### Ejemplo
567
+
568
+ ```typescript
569
+ // useFilters.ts
570
+ import { useUrlState } from 'state-in-url/react';
571
+
572
+ type FiltersState = { sort: 'name' | 'date'; page: number };
573
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
574
+
575
+ export function useFilters() {
576
+ return useUrlState(FILTERS_STATE);
577
+ }
578
+ ```
579
+
580
+ ```typescript
581
+ // FiltersBar.tsx
582
+ import { useFilters } from './useFilters';
583
+
584
+ export function FiltersBar() {
585
+ const { urlState, setUrl } = useFilters();
586
+
587
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
588
+ }
589
+ ```
590
+
591
+ `state-in-url/astro` es este mismo hook; se documenta aparte más abajo porque las islands además reciben `searchParams` como prop.
592
+
543
593
  ### Hook useUrlState para Astro
544
594
 
545
595
  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.
@@ -818,9 +868,39 @@ Sincronizar el estado en `onBlur` estará más alineado con el uso en el mundo r
818
868
 
819
869
  ## Otros hooks y helpers
820
870
 
821
- ### Hook `useUrlStateBase` para otros routers
871
+ ### Hook `useUrlStateBase` para otro router
872
+
873
+ Ú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)`.
874
+
875
+ 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`.
876
+
877
+ ```typescript
878
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
879
+
880
+ type FiltersState = { sort: 'name' | 'date'; page: number };
881
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
882
+
883
+ export function useFilters() {
884
+ const router = useYourRouter();
885
+ const nav = React.useMemo(
886
+ () => ({
887
+ push: (url: string) => router.navigate(url),
888
+ replace: (url: string) => router.navigate(url, { replace: true }),
889
+ }),
890
+ [router],
891
+ );
892
+
893
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
894
+ }
895
+ ```
896
+
897
+ Dos diferencias respecto a `useUrlState`:
898
+
899
+ - Devuelve `state`, `updateUrl` y `updateState`, no `urlState`, `setUrl` ni `setState`; además `updateUrl` hace push por defecto, mientras que `setUrl` hace replace.
900
+ - 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.
822
901
 
823
- Hooks para crear tus propios hooks `useUrlState` con otros routers, p. ej. react-router o tanstack router.
902
+ > [!NOTE]
903
+ > `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`.
824
904
 
825
905
  [Documentación de la API](packages/urlstate/useUrlStateBase)
826
906
 
@@ -917,6 +997,41 @@ export const useUserState = () => {
917
997
  2. Los servidores de Vercel limitan el tamaño de las cabeceras (la cadena de consulta y demás) a **14KB**, así que mantén el estado de tu URL por debajo de ~5000 palabras. <https://vercel.com/docs/errors/URL_TOO_LONG>
918
998
  3. Probado con `next.js` 14/15/16 con app router; no hay planes de soportar pages.
919
999
 
1000
+ 4. **Vitest + react-router 7**: si una prueba que renderiza un componente dentro de `<BrowserRouter>` lanza `useNavigate() may be used only in the context of a <Router> component` con el provider justo ahí, actualiza a `react-router@8` o incorpora este paquete en línea:
1001
+
1002
+ ```ts
1003
+ // vitest.config.ts
1004
+ test: { server: { deps: { inline: ['state-in-url'] } } }
1005
+ ```
1006
+
1007
+ Vitest externaliza `node_modules`, así que Node carga `state-in-url` mientras Vite carga tu archivo de prueba. En `react-router@7` la condición de exportación `node` apunta `module`/`module-sync` a `index.mjs` y `default` a `index.js`, y los dos resolutores eligen distinto: react-router se carga dos veces, su contexto de React existe por duplicado y el provider escribe en la copia que el hook no lee. Incorporarlo en línea pone ambos lados en el resolutor de Vite.
1008
+
1009
+ `react-router@8` apunta `default` y `module-sync` al mismo archivo, de modo que ningún resolutor puede discrepar y no hace falta nada. El error nunca menciona `state-in-url`, por eso esto está aquí.
1010
+
1011
+ 5. **Jest**: este paquete se resuelve solo como ESM y Jest ejecuta las pruebas como CommonJS por defecto, así que un `require()` simple falla con `Must use import to load ES Module`. Jest no hereda el `require(esm)` sin flag de Node — usa su propia API síncrona `vm` —, así que en **Node 24.9+** pásale el flag:
1012
+
1013
+ ```json
1014
+ // package.json
1015
+ "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" }
1016
+ ```
1017
+
1018
+ Si prefieres no pasar un flag, `dist/` también incluye una compilación CommonJS. Está deliberadamente fuera del mapa `exports`, así que nada se resuelve a ella por accidente: apunta Jest a ella de forma explícita:
1019
+
1020
+ ```js
1021
+ // jest.config.js
1022
+ moduleNameMapper: {
1023
+ '^state-in-url$': '<rootDir>/node_modules/state-in-url/dist/index.cjs',
1024
+ '^state-in-url/utils$': '<rootDir>/node_modules/state-in-url/dist/utils.cjs',
1025
+ '^state-in-url/(.*)$': '<rootDir>/node_modules/state-in-url/dist/$1/index.cjs',
1026
+ },
1027
+ ```
1028
+
1029
+ Eso sustituye la compilación por la de CJS en todo el proceso de pruebas, y es seguro. No mezcles las dos en un mismo proceso: este paquete guarda su almacén de estado compartido en el ámbito del módulo, así que dos copias significan dos almacenes y `useSharedState` deja de compartir sin avisar.
1030
+
1031
+ Una diferencia más: `jest.mock()` no se aplica a los módulos ES. Usa `jest.unstable_mockModule()` si haces mock de este paquete.
1032
+
1033
+ Vitest, Next.js, Vite, Astro y Remix no necesitan nada de esta configuración de Jest. El único paso adicional de Vitest es el `server.deps.inline` de arriba, y solo con `react-router@7`.
1034
+
920
1035
  ## Otros
921
1036
 
922
1037
  ### Contribuir y/o ejecutar localmente
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=4d98ba970e6aab8f29987fbcba29fb6c61c2fc67 status=translated -->
3
+ <!-- i18n:meta locale=fr source=README.md source-blob=8b11ee8f8c8b83c207a940455766e24376508130 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -138,6 +138,7 @@ La comparaison complète — la même fonctionnalité dans les deux, d’autres
138
138
  - [Installation](#installation)
139
139
  - [1. Installer le paquet](#1-installer-le-paquet)
140
140
  - [2. Modifier tsconfig.json](#2-modifier-tsconfigjson)
141
+ - [3. Remarque : ce paquet ne se résout qu'en ESM](#3-remarque--ce-paquet-ne-se-résout-quen-esm)
141
142
  - [Utilisation avec des agents de codage IA](#utilisation-avec-des-agents-de-codage-ia)
142
143
  - [useUrlState](#useurlstate)
143
144
  - [Hook useUrlState pour Next.js](#hook-useurlstate-pour-nextjs)
@@ -150,14 +151,16 @@ La comparaison complète — la même fonctionnalité dans les deux, d’autres
150
151
  - [Exemple](#exemple)
151
152
  - [Hook useUrlState pour React-Router](#hook-useurlstate-pour-react-router)
152
153
  - [Exemple](#exemple-1)
153
- - [Hook useUrlState pour Astro](#hook-useurlstate-pour-astro)
154
+ - [Hook useUrlState pour React (sans routeur)](#hook-useurlstate-pour-react-sans-routeur)
154
155
  - [Exemple](#exemple-2)
156
+ - [Hook useUrlState pour Astro](#hook-useurlstate-pour-astro)
157
+ - [Exemple](#exemple-3)
155
158
  - [Recettes](#recettes)
156
159
  - [Hook personnalisé pour travailler confortablement avec une tranche d'état](#hook-personnalisé-pour-travailler-confortablement-avec-une-tranche-détat)
157
160
  - [Avec une forme d'état complexe](#avec-une-forme-détat-complexe)
158
161
  - [Mettre à jour l'état seul et le synchroniser manuellement avec l'URL](#mettre-à-jour-létat-seul-et-le-synchroniser-manuellement-avec-lurl)
159
162
  - [Autres hooks et helpers](#autres-hooks-et-helpers)
160
- - [Hook `useUrlStateBase` pour d'autres routeurs](#hook-useurlstatebase-pour-dautres-routeurs)
163
+ - [Hook `useUrlStateBase` pour un autre routeur](#hook-useurlstatebase-pour-un-autre-routeur)
161
164
  - [Hook `useSharedState` pour React.js](#hook-usesharedstate-pour-reactjs)
162
165
  - [Hook `useLinkProps` pour React.js](#hook-uselinkprops-pour-reactjs)
163
166
  - [Hook `useUrlEncode` pour React.js](#hook-useurlencode-pour-reactjs)
@@ -195,6 +198,20 @@ pnpm add state-in-url
195
198
  Dans `tsconfig.json`, dans `compilerOptions`, définissez `"moduleResolution": "Bundler"`, ou `"moduleResolution": "Node16"`, ou `"moduleResolution": "NodeNext"`.
196
199
  Il peut être nécessaire de définir `"module": "ES2022"`, ou `"module": "ESNext"`
197
200
 
201
+ ### 3. Remarque : ce paquet ne se résout qu'en ESM
202
+
203
+ Next.js, Vite, Astro et Remix l'empaquettent sans aucune configuration, et
204
+ `require()` depuis Node seul fonctionne sur Node `^20.19` ou `>=22.12`, où
205
+ `require(esm)` ne demande pas de drapeau.
206
+
207
+ Une build CommonJS est également publiée dans `dist/` avec l'extension `.cjs`,
208
+ mais la table `exports` ne pointe pas dessus : aucune résolution n'y aboutit par
209
+ accident. Elle existe pour les cas limites — un Node ancien, ou Jest sans
210
+ drapeau — et on ne l'atteint que par un chemin explicite.
211
+
212
+ **Dans les deux cas, Jest demande une ligne de configuration** — voir le point 5
213
+ des [Pièges](#pièges).
214
+
198
215
  ## Utilisation avec des agents de codage IA
199
216
 
200
217
  `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 :
@@ -540,6 +557,39 @@ const tags = [
540
557
 
541
558
  [Exemple de code](packages/example-react-router6/src/Form-for-test.tsx)
542
559
 
560
+ ### Hook useUrlState pour React (sans routeur)
561
+
562
+ 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.
563
+
564
+ [Documentation de l'API](packages/urlstate/react/useUrlState)
565
+
566
+ #### Exemple
567
+
568
+ ```typescript
569
+ // useFilters.ts
570
+ import { useUrlState } from 'state-in-url/react';
571
+
572
+ type FiltersState = { sort: 'name' | 'date'; page: number };
573
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
574
+
575
+ export function useFilters() {
576
+ return useUrlState(FILTERS_STATE);
577
+ }
578
+ ```
579
+
580
+ ```typescript
581
+ // FiltersBar.tsx
582
+ import { useFilters } from './useFilters';
583
+
584
+ export function FiltersBar() {
585
+ const { urlState, setUrl } = useFilters();
586
+
587
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
588
+ }
589
+ ```
590
+
591
+ `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.
592
+
543
593
  ### Hook useUrlState pour Astro
544
594
 
545
595
  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.
@@ -818,9 +868,39 @@ Synchroniser l'état sur `onBlur` sera plus conforme à l'usage réel.
818
868
 
819
869
  ## Autres hooks et helpers
820
870
 
821
- ### Hook `useUrlStateBase` pour d'autres routeurs
871
+ ### Hook `useUrlStateBase` pour un autre routeur
872
+
873
+ 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)`.
874
+
875
+ 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`.
876
+
877
+ ```typescript
878
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
879
+
880
+ type FiltersState = { sort: 'name' | 'date'; page: number };
881
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
882
+
883
+ export function useFilters() {
884
+ const router = useYourRouter();
885
+ const nav = React.useMemo(
886
+ () => ({
887
+ push: (url: string) => router.navigate(url),
888
+ replace: (url: string) => router.navigate(url, { replace: true }),
889
+ }),
890
+ [router],
891
+ );
892
+
893
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
894
+ }
895
+ ```
896
+
897
+ Deux différences avec `useUrlState` :
898
+
899
+ - 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.
900
+ - 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.
822
901
 
823
- Hooks pour créer vos propres hooks `useUrlState` avec d'autres routeurs, par exemple react-router ou tanstack router.
902
+ > [!NOTE]
903
+ > `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`.
824
904
 
825
905
  [Documentation de l'API](packages/urlstate/useUrlStateBase)
826
906
 
@@ -917,6 +997,41 @@ export const useUserState = () => {
917
997
  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>
918
998
  3. Testé avec `next.js` 14/15/16 avec app router ; pas de plan pour prendre en charge pages.
919
999
 
1000
+ 4. **Vitest + react-router 7** : si un test qui rend un composant à l'intérieur de `<BrowserRouter>` lève `useNavigate() may be used only in the context of a <Router> component` alors que le provider est juste là, passez à `react-router@8` ou intégrez ce paquet en ligne :
1001
+
1002
+ ```ts
1003
+ // vitest.config.ts
1004
+ test: { server: { deps: { inline: ['state-in-url'] } } }
1005
+ ```
1006
+
1007
+ Vitest externalise `node_modules`, donc Node charge `state-in-url` tandis que Vite charge votre fichier de test. Dans `react-router@7`, la condition d'export `node` pointe `module`/`module-sync` vers `index.mjs` et `default` vers `index.js`, et les deux résolveurs n'en choisissent pas le même : react-router est chargé deux fois, son contexte React existe en double, et le provider écrit dans la copie que le hook ne lit pas. L'intégration en ligne place les deux côtés sur le résolveur de Vite.
1008
+
1009
+ `react-router@8` pointe `default` et `module-sync` vers le même fichier : aucun résolveur ne peut diverger et rien n'est nécessaire. L'erreur ne nomme jamais `state-in-url`, d'où la présence de ce point ici.
1010
+
1011
+ 5. **Jest** : ce paquet ne se résout qu'en ESM, et Jest exécute les tests en CommonJS par défaut ; un simple `require()` échoue donc avec `Must use import to load ES Module`. Jest n'hérite pas du `require(esm)` sans drapeau de Node — il utilise sa propre API synchrone `vm` — donc sur **Node 24.9+**, passez-lui le drapeau :
1012
+
1013
+ ```json
1014
+ // package.json
1015
+ "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" }
1016
+ ```
1017
+
1018
+ Si vous préférez éviter le drapeau, `dist/` contient aussi une build CommonJS. Elle est délibérément absente de la table `exports`, si bien que rien ne s'y résout par accident : pointez Jest dessus explicitement :
1019
+
1020
+ ```js
1021
+ // jest.config.js
1022
+ moduleNameMapper: {
1023
+ '^state-in-url$': '<rootDir>/node_modules/state-in-url/dist/index.cjs',
1024
+ '^state-in-url/utils$': '<rootDir>/node_modules/state-in-url/dist/utils.cjs',
1025
+ '^state-in-url/(.*)$': '<rootDir>/node_modules/state-in-url/dist/$1/index.cjs',
1026
+ },
1027
+ ```
1028
+
1029
+ Cela substitue la build CJS pour tout le processus de test, et c'est sans danger. Ne mélangez pas les deux dans un même processus : ce paquet garde son magasin d'état partagé dans la portée du module, donc deux copies signifient deux magasins et `useSharedState` cesse de partager sans rien signaler.
1030
+
1031
+ Autre différence : `jest.mock()` ne s'applique pas aux modules ES. Utilisez `jest.unstable_mockModule()` si vous mockez ce paquet.
1032
+
1033
+ Vitest, Next.js, Vite, Astro et Remix n'ont besoin de rien de cette configuration Jest. La seule étape supplémentaire pour Vitest est le `server.deps.inline` ci-dessus, et uniquement avec `react-router@7`.
1034
+
920
1035
  ## Autres
921
1036
 
922
1037
  ### Contribuer et/ou exécuter localement
package/README.ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.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](./README.vi.md)
3
- <!-- i18n:meta locale=ja source=README.md source-blob=4d98ba970e6aab8f29987fbcba29fb6c61c2fc67 status=translated -->
3
+ <!-- i18n:meta locale=ja source=README.md source-blob=8b11ee8f8c8b83c207a940455766e24376508130 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -138,6 +138,7 @@ nuqs も優れたライブラリです。値ごとに読みやすいクエリパ
138
138
  - [インストール](#インストール)
139
139
  - [1. パッケージのインストール](#1-パッケージのインストール)
140
140
  - [2. tsconfig.json の編集](#2-tsconfigjson-の編集)
141
+ - [3. 補足: このパッケージは ESM としてのみ解決されます](#3-補足-このパッケージは-esm-としてのみ解決されます)
141
142
  - [AI コーディングエージェントと使う](#ai-コーディングエージェントと使う)
142
143
  - [useUrlState](#useurlstate)
143
144
  - [Next.js 向け useUrlState フック](#nextjs-向け-useurlstate-フック)
@@ -150,8 +151,10 @@ nuqs も優れたライブラリです。値ごとに読みやすいクエリパ
150
151
  - [例](#例)
151
152
  - [React-Router 向け useUrlState フック](#react-router-向け-useurlstate-フック)
152
153
  - [例](#例-1)
153
- - [Astro 向け useUrlState フック](#astro-向け-useurlstate-フック)
154
+ - [React 向け useUrlState フック(ルーターなし)](#react-向け-useurlstate-フックルーターなし)
154
155
  - [例](#例-2)
156
+ - [Astro 向け useUrlState フック](#astro-向け-useurlstate-フック)
157
+ - [例](#例-3)
155
158
  - [レシピ](#レシピ)
156
159
  - [状態の一部を便利に扱うカスタムフック](#状態の一部を便利に扱うカスタムフック)
157
160
  - [複雑な状態の形を使用](#複雑な状態の形を使用)
@@ -195,6 +198,19 @@ pnpm add state-in-url
195
198
  `tsconfig.json` の `compilerOptions` に `"moduleResolution": "Bundler"`、または `"moduleResolution": "Node16"`、または `"moduleResolution": "NodeNext"` を設定します。
196
199
  `"module": "ES2022"` または `"module": "ESNext"` の設定が必要になる場合があります。
197
200
 
201
+ ### 3. 補足: このパッケージは ESM としてのみ解決されます
202
+
203
+ Next.js、Vite、Astro、Remix は設定なしでバンドルできます。また `require(esm)` が
204
+ フラグなしで使える Node `^20.19` または `>=22.12` であれば、素の Node からの
205
+ `require()` も動作します。
206
+
207
+ `dist/` には `.cjs` という拡張子で CommonJS ビルドも同梱されていますが、`exports`
208
+ マップはそれを指していないため、解決が偶然そちらへ向かうことはありません。これは
209
+ 例外的なケース——古い Node、あるいはフラグなしの Jest——のためのもので、明示的な
210
+ パスでのみ参照できます。
211
+
212
+ **どちらの場合も Jest には 1 行の設定が必要です**——[注意点](#注意点)の 5 番を参照してください。
213
+
198
214
  ## AI コーディングエージェントと使う
199
215
 
200
216
  `state-in-url` は [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview) 向けのスキルファイルを同梱しています。AI エージェント(Claude Code、Cursor、Copilot、Codex など)がライブラリ使用時の正しいパターンを読み込み、よくあるミスを避けられます。`state-in-url` をインストールした後、プロジェクトで一度実行してください:
@@ -540,6 +556,39 @@ const tags = [
540
556
 
541
557
  [サンプルコード](packages/example-react-router6/src/Form-for-test.tsx)
542
558
 
559
+ ### React 向け useUrlState フック(ルーターなし)
560
+
561
+ ルーターを持たない React アプリ向けです。Vite、Create React App、あるいは自分では管理できないページに埋め込むウィジェットなどが対象です。このフックは `window.history` で URL を書き込み、戻る/進むや他の `pushState`/`replaceState` のたびに読み戻します。同じ state オブジェクトを使うコンポーネントはすべて状態を共有するため、ラップするものも props で渡すものもありません。
562
+
563
+ [API ドキュメント](packages/urlstate/react/useUrlState)
564
+
565
+ #### 例
566
+
567
+ ```typescript
568
+ // useFilters.ts
569
+ import { useUrlState } from 'state-in-url/react';
570
+
571
+ type FiltersState = { sort: 'name' | 'date'; page: number };
572
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
573
+
574
+ export function useFilters() {
575
+ return useUrlState(FILTERS_STATE);
576
+ }
577
+ ```
578
+
579
+ ```typescript
580
+ // FiltersBar.tsx
581
+ import { useFilters } from './useFilters';
582
+
583
+ export function FiltersBar() {
584
+ const { urlState, setUrl } = useFilters();
585
+
586
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
587
+ }
588
+ ```
589
+
590
+ `state-in-url/astro` は同じフックです。islands では `searchParams` を prop として受け取るため、下で別に説明しています。
591
+
543
592
  ### Astro 向け useUrlState フック
544
593
 
545
594
  React アイランド向けです。Astro にはデフォルトでクライアントサイドルーターがないため、このフックは `window.history` で URL を書き込み、戻る/進む操作時およびその他あらゆる `pushState`/`replaceState` の際(Astro 自身の `<ClientRouter />` を含む)に URL を読み戻します。ページ上のアイランドは状態を共有し、それらをラップするものは何も必要ありません。
@@ -820,7 +869,37 @@ const tags = [
820
869
 
821
870
  ### 他のルーター向け `useUrlStateBase` フック
822
871
 
823
- react-router や tanstack router など、他のルーターで独自の `useUrlState` フックを作成するためのフックです。
872
+ 本パッケージがエントリーポイントを用意していないルーター(例: TanStack Router)向けに、自前の `useUrlState` を作るために使います。`push(url)` と `replace(url)` を持つメモ化されたオブジェクトを渡してください。
873
+
874
+ ルーターが**まったくない**場合は不要です。[`state-in-url/react`](#react-向け-useurlstate-フックルーターなし) を使ってください。こちらは同じフックを `window.history` に接続済みのものです。
875
+
876
+ ```typescript
877
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
878
+
879
+ type FiltersState = { sort: 'name' | 'date'; page: number };
880
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
881
+
882
+ export function useFilters() {
883
+ const router = useYourRouter();
884
+ const nav = React.useMemo(
885
+ () => ({
886
+ push: (url: string) => router.navigate(url),
887
+ replace: (url: string) => router.navigate(url, { replace: true }),
888
+ }),
889
+ [router],
890
+ );
891
+
892
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
893
+ }
894
+ ```
895
+
896
+ `useUrlState` との違いは 2 点です:
897
+
898
+ - 返すのは `urlState`、`setUrl`、`setState` ではなく `state`、`updateUrl`、`updateState` です。また `updateUrl` の既定は push で、`setUrl` の既定は replace です。
899
+ - 第 3 引数が初期 state を決めます。省略するとフックは読み込み時のクエリ文字列を無視するため、共有されたリンクを開いても既定値が表示されます。
900
+
901
+ > [!NOTE]
902
+ > `useUrlState` はパッケージのルートからはエクスポートされません。どの `useUrlState` もフレームワークに結び付いているため、それぞれ専用のエントリーポイント(`state-in-url/next`、`/react`、`/react-router`、`/react-router6`、`/remix`、`/astro`)にあります。ルートエントリーにはフレームワークに依存しないものだけが残ります: `useSharedState`、`useUrlEncode`、`useUrlStateBase`、`useLinkProps`、`encode`/`decode`、`encodeState`/`decodeState`。
824
903
 
825
904
  [API ドキュメント](packages/urlstate/useUrlStateBase)
826
905
 
@@ -917,6 +996,41 @@ export const useUserState = () => {
917
996
  2. Vercel サーバーはヘッダー(クエリ文字列など)のサイズを **14KB** に制限しているため、URL の状態は約 5000 語未満に保ってください。<https://vercel.com/docs/errors/URL_TOO_LONG>
918
997
  3. app router 付きの `next.js` 14/15/16 でテスト済みです。pages をサポートする予定はありません。
919
998
 
999
+ 4. **Vitest + react-router 7**: provider がすぐそこにあるのに、`<BrowserRouter>` の内側でコンポーネントを描画するテストが `useNavigate() may be used only in the context of a <Router> component` を投げる場合は、`react-router@8` に上げるか、このパッケージをインライン化してください:
1000
+
1001
+ ```ts
1002
+ // vitest.config.ts
1003
+ test: { server: { deps: { inline: ['state-in-url'] } } }
1004
+ ```
1005
+
1006
+ Vitest は `node_modules` を外部化するため、`state-in-url` は Node が、テストファイルは Vite が読み込みます。`react-router@7` では `node` エクスポート条件が `module`/`module-sync` を `index.mjs` に、`default` を `index.js` に向けており、2 つのリゾルバがそれぞれ別のものを選びます。その結果 react-router が二重に読み込まれ、React context も二つ存在し、provider が書き込むのは hook が読まない方のコピーになります。インライン化すると双方が Vite のリゾルバに揃います。
1007
+
1008
+ `react-router@8` は `default` と `module-sync` を同じファイルに向けているので、どのリゾルバも食い違いようがなく、何も必要ありません。エラーメッセージに `state-in-url` の名前は出てこないため、ここに書いてあります。
1009
+
1010
+ 5. **Jest**: このパッケージは ESM としてのみ解決されますが、Jest は既定でテストを CommonJS として実行するため、素の `require()` は `Must use import to load ES Module` で失敗します。Jest は Node のフラグ不要な `require(esm)` を引き継がず、独自の同期 `vm` API を使うので、**Node 24.9+** ではフラグを渡してください:
1011
+
1012
+ ```json
1013
+ // package.json
1014
+ "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" }
1015
+ ```
1016
+
1017
+ フラグを渡したくない場合、`dist/` には CommonJS ビルドも同梱されています。`exports` マップからは意図的に外してあるため偶然解決されることはありません——Jest を明示的にそこへ向けてください:
1018
+
1019
+ ```js
1020
+ // jest.config.js
1021
+ moduleNameMapper: {
1022
+ '^state-in-url$': '<rootDir>/node_modules/state-in-url/dist/index.cjs',
1023
+ '^state-in-url/utils$': '<rootDir>/node_modules/state-in-url/dist/utils.cjs',
1024
+ '^state-in-url/(.*)$': '<rootDir>/node_modules/state-in-url/dist/$1/index.cjs',
1025
+ },
1026
+ ```
1027
+
1028
+ これはテストプロセス全体を CJS ビルドに差し替えるもので、安全です。一つのプロセス内で両方を混在させないでください——このパッケージは共有ステートのストアをモジュールスコープに置いているため、コピーが二つあればストアも二つになり、`useSharedState` は何も告げずに共有をやめます。
1029
+
1030
+ もう一つの違いとして、`jest.mock()` は ES モジュールには効きません。このパッケージをモックするなら `jest.unstable_mockModule()` を使ってください。
1031
+
1032
+ Vitest、Next.js、Vite、Astro、Remix はいずれもこの Jest 向けの設定を必要としません。Vitest で追加が必要なのは上記の `server.deps.inline` だけで、しかも `react-router@7` の場合のみです。
1033
+
920
1034
  ## その他
921
1035
 
922
1036
  ### 貢献、またはローカルでの実行