state-in-url 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.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 · [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 -->
4
+ <!-- i18n:end -->
5
+
6
+ <div align="center">
7
+ <img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="logotipo 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">No dudes en abrir una issue si encuentras un bug o quieres solicitar funciones</h4>
39
+
40
+ ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
41
+
42
+ # Demo
43
+
44
+ <a href="https://state-in-url.dev" target="_blank">Demo</a> |
45
+ <a href="https://state-in-url.netlify.app/" target="_blank">Enlace espejo</a>
46
+
47
+ <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">Limitación de tamaño de URI, <b>hasta 12KB</b> es seguro</a>
48
+
49
+ <hr />
50
+
51
+ ¡Añade una <a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a> y <a href="https://github.com/asmyshlyaev177" target="_blank">sígueme</a> para apoyar el proyecto!
52
+
53
+ Agradeceré tus comentarios/opiniones en [discusiones](https://github.com/asmyshlyaev177/state-in-url/discussions/1)
54
+
55
+ Comparte si te resulta útil.
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
+ [¡Muéstrame solo el código!](#useurlstate)
66
+
67
+ ## ¿Por qué usar `state-in-url`?
68
+
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
+ 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
+
72
+ Incluye el hook `useUrlState` para Next.js y react-router, y helpers para cualquier otra cosa en JS.
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
+
75
+ Es hora de usar la cadena de consulta para gestionar el estado, como se pensó originalmente.
76
+ Esta biblioteca hace todo el trabajo tedioso por ti.
77
+
78
+ Esta biblioteca es una buena alternativa a NUQS.
79
+
80
+ ### Casos de uso
81
+
82
+ - Guardar formularios de usuario no guardados o filtros de página en la URL
83
+ - Sincronizar la URL con el estado de React
84
+ - Solo sincronizar datos entre componentes de cliente no relacionados sin tocar la URI
85
+ - URLs compartibles con el estado de la aplicación (enlaces profundos, sincronización del estado en la URL)
86
+ - Persistencia sencilla del estado entre recargas de página
87
+
88
+ ### Características
89
+
90
+ - 🧩 **Simple**: Sin providers, reducers, código repetitivo ni conceptos nuevos, API similar a `React.useState`
91
+ - 📘 **Validación/autocompletado de TypeScript**: El estado es solo un objeto, validación estática automática en el IDE/pruebas según la definición de TypeScript
92
+ - ✨ **Datos complejos**: Objetos anidados, fechas y matrices, funciona igual que JSON, pero en la URL
93
+ - ☂ **Valores por defecto**: Te da valores por defecto si el parámetro no está en la url
94
+ - ⌨ **Organizado**: Todos los valores posibles se definen al inicio, te protege de obtener una clave inexistente
95
+ - **compatible**: Mantendrá los parámetros de consulta de terceros tal cual
96
+ - **flexible**: Puede usar más de 1 objeto de estado en la misma página, solo usa claves diferentes
97
+ - **Rápido**: Mínimos re-renders, alrededor de [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) para codificar y decodificar un objeto grande
98
+ - **Renderizado en el servidor**: Se puede usar en componentes de servidor, se soportan Next.js 14, 15 y 16
99
+ - **Ligero**: Cero dependencias, biblioteca de menos de 2KB
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
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
+ - **Licencia permisiva**: MIT
104
+
105
+ ## Tabla de contenido
106
+
107
+ <!-- toc:start -->
108
+
109
+ - [State in url](#state-in-url)
110
+ - [Demo](#demo)
111
+ - [¿Por qué usar `state-in-url`?](#por-qué-usar-state-in-url)
112
+ - [Casos de uso](#casos-de-uso)
113
+ - [Características](#características)
114
+ - [Tabla de contenido](#tabla-de-contenido)
115
+ - [Instalación](#instalación)
116
+ - [1. Instalar el paquete](#1-instalar-el-paquete)
117
+ - [2. Editar tsconfig.json](#2-editar-tsconfigjson)
118
+ - [Uso con agentes de codificación con IA](#uso-con-agentes-de-codificación-con-ia)
119
+ - [useUrlState](#useurlstate)
120
+ - [Hook useUrlState para Next.js](#hook-useurlstate-para-nextjs)
121
+ - [Ejemplos de uso](#ejemplos-de-uso)
122
+ - [Básico](#básico)
123
+ - [Con renderizado del lado del servidor](#con-renderizado-del-lado-del-servidor)
124
+ - [Usar el hook en el componente `layout`](#usar-el-hook-en-el-componente-layout)
125
+ - [Con una forma de estado arbitraria (no recomendado)](#con-una-forma-de-estado-arbitraria-no-recomendado)
126
+ - [Hook useUrlState para Remix.js](#hook-useurlstate-para-remixjs)
127
+ - [Ejemplo](#ejemplo)
128
+ - [Hook useUrlState para React-Router](#hook-useurlstate-para-react-router)
129
+ - [Ejemplo](#ejemplo-1)
130
+ - [Recetas](#recetas)
131
+ - [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
+ - [Con una forma de estado compleja](#con-una-forma-de-estado-compleja)
133
+ - [Actualizar solo el estado y sincronizarlo manualmente con la URL](#actualizar-solo-el-estado-y-sincronizarlo-manualmente-con-la-url)
134
+ - [Otros hooks y helpers](#otros-hooks-y-helpers)
135
+ - [Hook `useUrlStateBase` para otros routers](#hook-useurlstatebase-para-otros-routers)
136
+ - [Hook `useSharedState` para React.js](#hook-usesharedstate-para-reactjs)
137
+ - [Hook `useLinkProps` para React.js](#hook-uselinkprops-para-reactjs)
138
+ - [Hook `useUrlEncode` para React.js](#hook-useurlencode-para-reactjs)
139
+ - [Helpers `encodeState` y `decodeState`](#helpers-encodestate-y-decodestate)
140
+ - [Helpers `encode` y `decode`](#helpers-encode-y-decode)
141
+ - [Buenas prácticas](#buenas-prácticas)
142
+ - [Trampas](#trampas)
143
+ - [Otros](#otros)
144
+ - [Contribuir y/o ejecutar localmente](#contribuir-yo-ejecutar-localmente)
145
+ - [Hoja de ruta](#hoja-de-ruta)
146
+ - [Contacto y soporte](#contacto-y-soporte)
147
+ - [Registro de cambios](#registro-de-cambios)
148
+ - [Menciones](#menciones)
149
+ - [Licencia](#licencia)
150
+ - [Contrátame](#contrátame)
151
+ - [Inspiración](#inspiración)
152
+
153
+ <!-- toc:end -->
154
+
155
+ ## Instalación
156
+
157
+ ### 1. Instalar el paquete
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. Editar tsconfig.json
169
+
170
+ En `tsconfig.json`, en `compilerOptions`, establece `"moduleResolution": "Bundler"`, o `"moduleResolution": "Node16"`, o `"moduleResolution": "NodeNext"`.
171
+ Posiblemente necesites establecer `"module": "ES2022"`, o `"module": "ESNext"`
172
+
173
+ ## Uso con agentes de codificación con IA
174
+
175
+ `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:
176
+
177
+ ```sh
178
+ npx @tanstack/intent@latest install
179
+ ```
180
+
181
+ Esto configura tu agente instalado para que descubra las habilidades desde `node_modules/state-in-url/skills/`. Lista las habilidades disponibles con `npx @tanstack/intent@latest list`.
182
+
183
+ ## useUrlState
184
+
185
+ Hook principal que toma el estado inicial como parámetro y devuelve el objeto de estado, un callback para actualizar la url y un callback para actualizar solo el estado.
186
+ Todos los componentes que usan el mismo objeto `state` se sincronizan automáticamente.
187
+
188
+ ### Hook useUrlState para Next.js
189
+
190
+ [Documentación completa de la API](packages/urlstate/next/useUrlState)
191
+
192
+ [Ejemplo de React-Router](#hook-useurlstate-para-react-router)
193
+
194
+ #### Ejemplos de uso
195
+
196
+ ##### Básico
197
+
198
+ 1. Define la forma del estado con valores por defecto
199
+
200
+ ```typescript
201
+ // userState.ts
202
+ // Solo los parámetros con valor distinto al predeterminado irán a la url.
203
+ export const userState: UserState = { name: '', age: 0 }
204
+
205
+ // ¡Usa `Type`, no `Interface`!
206
+ type UserState = { name: string, age: number }
207
+ ```
208
+
209
+ 2. Impórtalo y úsalo
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
+ // puedes pasar el argumento `replace`, controla si `setUrl` usará `router.push` o `router.replace`, por defecto replace=true
219
+ // puedes pasar `searchParams` desde componentes de servidor, pasa `useHistory: false` si necesitas obtener algo en el componente de servidor
220
+ const { urlState, setUrl, setState } = useUrlState(userState);
221
+
222
+ return (
223
+ <div>
224
+ // urlState.name devolverá el valor por defecto de `userState` si la url está vacía
225
+ <input value={urlState.name}
226
+ // misma api que React.useState, p. ej. 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
+ // Puede actualizar el estado inmediatamente pero sincronizar el cambio con la url según sea necesario
236
+ onBlur={() => setUrl()}
237
+ />
238
+
239
+ <button onClick={() => setUrl((_, initial) => initial)}>
240
+ Reset
241
+ </button>
242
+
243
+ </div>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ##### Con renderizado del lado del servidor
249
+
250
+ <details>
251
+ <Summary>Ejemplo</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
+ ##### Usar el hook en el componente `layout`
274
+
275
+ <details>
276
+ <Summary>Ejemplo</Summary>
277
+ Eso es una parte delicada, ya que nextjs con app router no permite acceder a searchParams desde el lado del servidor. Hay una solución alternativa usando middleware, pero no es elegante y puede dejar de funcionar tras una actualización de nextjs.
278
+
279
+ ```typescript
280
+ // añadir al `layout.tsx` apropiado
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
+ // Componente layout de destino
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
+ ##### Con una forma de estado arbitraria (no recomendado)
325
+
326
+ <details>
327
+ <Summary>Ejemplo</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 para Remix.js
343
+
344
+ La API es la misma que la de la versión para Next.js, excepto que se pueden pasar opciones del tipo [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
345
+
346
+ [Documentación de la API](packages/urlstate/remix/useUrlState)
347
+
348
+ #### Ejemplo
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
+ // Puede actualizar el estado inmediatamente pero sincronizar el cambio con la url según sea necesario
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
+ [Ejemplo de código](packages/example-remix2/app/routes/Form-for-test.tsx)
428
+
429
+ ### Hook useUrlState para React-Router
430
+
431
+ La API es la misma que la de la versión para Next.js, excepto que se pueden pasar opciones del tipo [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
432
+
433
+ [Documentación de la API](packages/urlstate/react-router/useUrlState)
434
+
435
+ #### Ejemplo
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
+ // para 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
+ // Puede actualizar el estado inmediatamente pero sincronizar el cambio con la url según sea necesario
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
+ [Ejemplo de código](packages/example-react-router6/src/Form-for-test.tsx)
517
+
518
+ ## Recetas
519
+ ##### Hook personalizado para trabajar cómodamente con una porción del estado
520
+ <details>
521
+ <Summary>Ejemplo</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 primera navegación añadirá una nueva entrada al historial
549
+ // todas las siguientes solo reemplazarán esa entrada
550
+ // de esta forma el historial tendrá solo 2 entradas - ['/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
+ ##### Con una forma de estado compleja
569
+
570
+ <details>
571
+ <Summary>Ejemplo</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` se inferirá del tipo 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
+ [Ejemplo de código de la página de demo](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
646
+ </details>
647
+
648
+ ##### Actualizar solo el estado y sincronizarlo manualmente con la URL
649
+
650
+ <details>
651
+ <Summary>Ejemplo</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
+ // comparará el estado por contenido y no por referencia, y lanzará la actualización solo para valores nuevos
660
+ setUrl(urlState);
661
+ }, 500);
662
+
663
+ return () => {
664
+ clearTimeout(timer.current);
665
+ };
666
+ }, [urlState, setUrl]);
667
+ ```
668
+
669
+ Sincronizar el estado en `onBlur` estará más alineado con el uso en el mundo real.
670
+
671
+ ```typescript
672
+ <input onBlur={() => updateUrl()} .../>
673
+ ```
674
+
675
+ </details>
676
+
677
+ ## Otros hooks y helpers
678
+
679
+ ### Hook `useUrlStateBase` para otros routers
680
+
681
+ Hooks para crear tus propios hooks `useUrlState` con otros routers, p. ej. react-router o tanstack router.
682
+
683
+ [Documentación de la API](packages/urlstate/useUrlStateBase)
684
+
685
+ ### Hook `useSharedState` para React.js
686
+
687
+ Hook para compartir estado entre cualquier componente de React, probado con Next.js y 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
+ [Documentación de la API](packages/urlstate/useSharedState/README.es.md)
701
+
702
+ ### Hook `useLinkProps` para React.js
703
+
704
+ Hook para llevar el estado a un enlace que apunta a otra ruta, por ejemplo un selector de idioma. `setUrl` siempre escribe en la ruta actual; este no.
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
+ El marcado conserva el `href` tal cual, así que los rastreadores y `hreflang` ven la URL canónica; el estado se lee al hacer clic.
722
+
723
+ [Documentación de la API](packages/urlstate/useLinkProps/README.es.md)
724
+
725
+ ### Hook `useUrlEncode` para React.js
726
+
727
+ [Documentación de la API](packages/urlstate/useUrlEncode/README.es.md)
728
+
729
+ ### Helpers `encodeState` y `decodeState`
730
+
731
+ [Documentación de la API](packages/urlstate/encodeState/README.es.md)
732
+
733
+ ### Helpers `encode` y `decode`
734
+
735
+ [Documentación de la API](packages/urlstate/encoder/README.es.md)
736
+
737
+ ## Buenas prácticas
738
+
739
+ - Define la forma de tu estado como una constante
740
+ - Usa TypeScript para mayor seguridad de tipos y autocompletado
741
+ - Evita guardar información sensible en los parámetros de la URL (SSN, claves de API, etc.)
742
+ - Usa esta [extensión](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) para obtener errores de TS legibles
743
+
744
+ Puedes crear hooks de estado para porciones del estado y reutilizarlos en toda la aplicación. Por ejemplo:
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
+ // otra lógica
761
+
762
+ // restablecer los parámetros de consulta al navegar a otra página
763
+ React.useEffect(() => {
764
+ return reset
765
+ }, [])
766
+
767
+ return { userState: urlState, setUserState: setUrl };;
768
+ }
769
+
770
+ ```
771
+
772
+ ## Trampas
773
+
774
+ 1. Solo se pueden pasar valores serializables; `Function`, `BigInt` o `Symbol` no funcionarán, y probablemente tampoco cosas como `ArrayBuffer`. Todo lo que se pueda serializar a JSON funcionará.
775
+ 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>
776
+ 3. Probado con `next.js` 14/15/16 con app router; no hay planes de soportar pages.
777
+
778
+ ## Otros
779
+
780
+ ### Contribuir y/o ejecutar localmente
781
+
782
+ Consulta el [documento de contribución](CONTRIBUTING.md)
783
+
784
+ ## Hoja de ruta
785
+
786
+ - [x] hook para `Next.js`
787
+ - [x] hook para `react-router`
788
+ - [x] hook para `remix`
789
+ - [ ] hook para `svelte`
790
+ - [ ] hook para `astro`
791
+ - [ ] hook para guardar el estado en el hash ?
792
+
793
+ ## Contacto y soporte
794
+
795
+ - Crea un [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) para informes de errores, solicitudes de funciones o preguntas
796
+
797
+ ## [Registro de cambios](CHANGELOG.md)
798
+
799
+ ## Menciones
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
+ ## Licencia
806
+
807
+ Este proyecto está licenciado bajo la [licencia MIT](LICENSE).
808
+
809
+ ## Contrátame
810
+
811
+ Soy **Aleksandr Smyshliaev**, único autor y mantenedor de esta biblioteca.
812
+ Ingeniero frontend sénior (React / Next.js / TypeScript, más de 8 años) y **disponible para trabajo remoto a tiempo completo ahora mismo**.
813
+
814
+ Esta biblioteca es la versión resumida de lo que se me da bien: una API tipada sobre un primitivo de navegador desordenado, cero dependencias y estabilidad en Next.js, Remix y React Router a través de varias versiones mayores de React.
815
+
816
+ - **Se me da mejor** — bibliotecas de componentes, gestión de estado y suites de pruebas que sobreviven a un refactor.
817
+ - **También son míos** —
818
+ [react-horizontal-scrolling-menu](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu)
819
+ (~84k instalaciones semanales),
820
+ [test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
821
+ (grabación/reproducción para Playwright),
822
+ [llm-queue](https://github.com/asmyshlyaev177/llm-queue).
823
+ - **Dónde** — Tiflis, Georgia (GMT+4), solapamiento total con CET. Entidad contratista registrada, de modo que el compromiso B2B no necesita configuración de employer-of-record.
824
+ - **Contáctame** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
825
+ [asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
826
+ [LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
827
+
828
+ ## Inspiración
829
+
830
+ [NUQS](https://github.com/47ng/nuqs)
831
+
832
+ [Usar la URL para almacenar estado en Vue](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
833
+
834
+ [Almacenar el estado en la URL](https://antonz.org/storing-state/)
835
+
836
+ [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)