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.CN.md +4 -776
- package/README.es.md +836 -0
- package/README.fr.md +836 -0
- package/README.ja.md +836 -0
- package/{README.KO.md → README.ko.md} +69 -8
- package/README.md +34 -3
- package/README.pt-BR.md +836 -0
- package/README.ru.md +836 -0
- package/README.vi.md +836 -0
- package/README.zh-CN.md +836 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/package.json +28 -11
- package/dist/useLinkProps/hrefWithState.d.ts +3 -0
- package/dist/useLinkProps/hrefWithState.js +1 -0
- package/dist/useLinkProps/hrefWithState.mjs +1 -0
- package/dist/useLinkProps/index.d.ts +1 -0
- package/dist/useLinkProps/index.js +1 -0
- package/dist/useLinkProps/index.mjs +1 -0
- package/dist/useLinkProps/useLinkProps.d.ts +39 -0
- package/dist/useLinkProps/useLinkProps.js +1 -0
- package/dist/useLinkProps/useLinkProps.mjs +1 -0
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
- package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +28 -11
- package/skills/feature-state-hook/SKILL.md +15 -2
- package/skills/form-library-integration/SKILL.md +1 -1
- package/skills/input-handling/SKILL.md +1 -1
- package/skills/nextjs-ssr/SKILL.md +1 -1
- package/skills/react-router-remix-setup/SKILL.md +1 -1
- package/skills/shared-state-no-url/SKILL.md +1 -1
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
|
+
[](https://asmyshlyaev177.dev)
|
|
18
|
+
|
|
19
|
+
[](https://www.npmjs.com/package/state-in-url)
|
|
20
|
+

|
|
21
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
25
|
+
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
26
|
+
[]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
|
|
27
|
+
|
|
28
|
+
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
29
|
+
[](https://www.bestpractices.dev/projects/9679)
|
|
30
|
+
[](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
|
+

|
|
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)
|