state-in-url 6.2.4 → 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.
Files changed (51) hide show
  1. package/README.CN.md +4 -776
  2. package/README.es.md +836 -0
  3. package/README.fr.md +836 -0
  4. package/README.ja.md +836 -0
  5. package/{README.KO.md → README.ko.md} +69 -8
  6. package/README.md +34 -3
  7. package/README.pt-BR.md +836 -0
  8. package/README.ru.md +836 -0
  9. package/README.vi.md +836 -0
  10. package/README.zh-CN.md +836 -0
  11. package/dist/encoder/encoder.d.ts +1 -1
  12. package/dist/encoder/encoder.js +1 -1
  13. package/dist/encoder/encoder.mjs +1 -1
  14. package/dist/index.d.ts +9 -8
  15. package/dist/index.js +1 -1
  16. package/dist/index.mjs +1 -1
  17. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  18. package/dist/next/useUrlState/useUrlState.js +1 -1
  19. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  20. package/dist/package.json +34 -13
  21. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  22. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  23. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  24. package/dist/useInsertionEffect.js +1 -1
  25. package/dist/useLinkProps/hrefWithState.d.ts +3 -0
  26. package/dist/useLinkProps/hrefWithState.js +1 -0
  27. package/dist/useLinkProps/hrefWithState.mjs +1 -0
  28. package/dist/useLinkProps/index.d.ts +1 -0
  29. package/dist/useLinkProps/index.js +1 -0
  30. package/dist/useLinkProps/index.mjs +1 -0
  31. package/dist/useLinkProps/useLinkProps.d.ts +39 -0
  32. package/dist/useLinkProps/useLinkProps.js +1 -0
  33. package/dist/useLinkProps/useLinkProps.mjs +1 -0
  34. package/dist/useSharedState/useSharedState.d.ts +1 -1
  35. package/dist/useSharedState/useSharedState.js +1 -1
  36. package/dist/useSharedState/useSharedState.mjs +1 -1
  37. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  38. package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
  39. package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
  40. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  41. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  42. package/dist/utils.d.ts +12 -0
  43. package/dist/utils.js +1 -1
  44. package/dist/utils.mjs +1 -1
  45. package/package.json +34 -13
  46. package/skills/feature-state-hook/SKILL.md +36 -9
  47. package/skills/form-library-integration/SKILL.md +3 -4
  48. package/skills/input-handling/SKILL.md +20 -6
  49. package/skills/nextjs-ssr/SKILL.md +77 -9
  50. package/skills/react-router-remix-setup/SKILL.md +1 -1
  51. package/skills/shared-state-no-url/SKILL.md +1 -1
@@ -0,0 +1,836 @@
1
+ <!-- i18n:start -->
2
+ [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md) · Português (BR) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
3
+ <!-- i18n:meta locale=pt-BR 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 do state-in-url" width="120px" />
8
+
9
+ # State in url
10
+ </div>
11
+
12
+ <div align="center">
13
+ </div>
14
+
15
+ <div align="center">
16
+
17
+ [![Available for hire](https://img.shields.io/badge/available%20for%20hire-senior%20react%20engineer-2ea44f?style=flat-square)](https://asmyshlyaev177.dev)
18
+
19
+ [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
20
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
21
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
22
+
23
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
24
+ [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
25
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
26
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
27
+
28
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/asmyshlyaev177/state-in-url/badge)](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
29
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](https://www.bestpractices.dev/projects/9679)
30
+ [![license](https://img.shields.io/github/license/asmyshlyaev177/state-in-url.svg?style=flat-square)](https://github.com/asmyshlyaev177/state-in-url/blob/master/LICENSE)
31
+ <!-- or by embedding this in your HTML:
32
+ <a href="https://www.bestpractices.dev/projects/9679"><img src="https://www.bestpractices.dev/projects/9679/badge"></a> -->
33
+
34
+ </div>
35
+
36
+ <div align="center">
37
+
38
+ <h4 align="center">Não hesite em abrir uma issue se encontrou um bug ou para solicitar recursos</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">Link espelho</a>
46
+
47
+ <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">Limitação de tamanho da URI, <b>até 12KB</b> é seguro</a>
48
+
49
+ <hr />
50
+
51
+ Adicione uma <a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a> e <a href="https://github.com/asmyshlyaev177" target="_blank">siga-me</a> para apoiar o projeto!
52
+
53
+ Agradecerei seus comentários/opiniões nas [discussões](https://github.com/asmyshlyaev177/state-in-url/discussions/1)
54
+
55
+ Compartilhe se for útil para você.
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
+ [Só me mostre o código!](#useurlstate)
66
+
67
+ ## Por que usar `state-in-url`?
68
+
69
+ Armazene qualquer estado de usuário nos parâmetros de consulta; imagine JSON na URL de um navegador. Tudo isso mantendo os tipos e a estrutura dos dados: por exemplo, números serão decodificados como números e não como strings, datas como datas, etc., objetos e arrays são suportados.
70
+ Extremamente simples, rápido e com validação estática de TypeScript. Links profundos, ou seja, sincronização de URL, facilitados.
71
+
72
+ Contém o hook `useUrlState` para Next.js e react-router, e helpers para qualquer outra coisa em JS.
73
+ Como os navegadores modernos suportam URLs enormes e os usuários não se importam com as strings de consulta (é um fluxo de selecionar tudo e copiar/colar).
74
+
75
+ É hora de usar a string de consulta para gerenciamento de estado, como foi originalmente planejado.
76
+ Esta biblioteca faz todo o trabalho tedioso para você.
77
+
78
+ Esta biblioteca é uma boa alternativa ao NUQS.
79
+
80
+ ### Casos de uso
81
+
82
+ - Armazenar formulários de usuário não salvos ou filtros de página na URL
83
+ - Sincronizar a URL com o estado do React
84
+ - Apenas sincronizar dados entre componentes de cliente não relacionados sem tocar na URI
85
+ - URLs compartilháveis com o estado da aplicação (links profundos, sincronização do estado na URL)
86
+ - Persistência fácil do estado entre recarregamentos de página
87
+
88
+ ### Recursos
89
+
90
+ - 🧩 **Simples**: Sem providers, reducers, boilerplate ou novos conceitos, API semelhante a `React.useState`
91
+ - 📘 **Validação/autocompletar do TypeScript**: O estado é apenas um objeto, validação estática automática no IDE/testes de acordo com a definição do TypeScript
92
+ - ✨ **Dados complexos**: Objetos aninhados, datas e arrays, funciona como JSON, mas na URL
93
+ - ☂ **Valores padrão**: Fornece valores padrão se o parâmetro não estiver na url
94
+ - ⌨ **Organizado**: Todos os valores possíveis definidos no início, protege você de obter uma chave inexistente
95
+ - **compatível**: Manterá os parâmetros de consulta de terceiros como estão
96
+ - **flexível**: Pode usar mais de 1 objeto de estado na mesma página, basta usar chaves diferentes
97
+ - **Rápido**: Re-renderizações mínimas, cerca de [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) para codificar e decodificar objetos grandes
98
+ - **Renderização no servidor**: Pode ser usado em componentes de servidor, Next.js 14, 15 e 16 são suportados
99
+ - **Leve**: Zero dependências, biblioteca com menos de 2KB
100
+ - **DX**: Boa experiência de desenvolvimento, documentação, comentários JSDoc e exemplos
101
+ - **Flexibilidade de frameworks**: Hooks para `Next.js` e `react-router`, helpers para usar com outros frameworks ou JS puro
102
+ - **Bem testada**: [Testes unitários e testes Playwright para Chrome/Firefox/Safari](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
+ - **Licença permissiva**: MIT
104
+
105
+ ## Índice
106
+
107
+ <!-- toc:start -->
108
+
109
+ - [State in url](#state-in-url)
110
+ - [Demo](#demo)
111
+ - [Por que usar `state-in-url`?](#por-que-usar-state-in-url)
112
+ - [Casos de uso](#casos-de-uso)
113
+ - [Recursos](#recursos)
114
+ - [Índice](#índice)
115
+ - [Instalação](#instalação)
116
+ - [1. Instalar o pacote](#1-instalar-o-pacote)
117
+ - [2. Editar o tsconfig.json](#2-editar-o-tsconfigjson)
118
+ - [Uso com agentes de codificação de IA](#uso-com-agentes-de-codificação-de-ia)
119
+ - [useUrlState](#useurlstate)
120
+ - [Hook useUrlState para Next.js](#hook-useurlstate-para-nextjs)
121
+ - [Exemplos de uso](#exemplos-de-uso)
122
+ - [Básico](#básico)
123
+ - [Com renderização no servidor](#com-renderização-no-servidor)
124
+ - [Usando o hook no componente `layout`](#usando-o-hook-no-componente-layout)
125
+ - [Com forma de estado arbitrária (não recomendado)](#com-forma-de-estado-arbitrária-não-recomendado)
126
+ - [Hook useUrlState para Remix.js](#hook-useurlstate-para-remixjs)
127
+ - [Exemplo](#exemplo)
128
+ - [Hook useUrlState para React-Router](#hook-useurlstate-para-react-router)
129
+ - [Exemplo](#exemplo-1)
130
+ - [Receitas](#receitas)
131
+ - [Hook personalizado para trabalhar convenientemente com uma fatia do estado](#hook-personalizado-para-trabalhar-convenientemente-com-uma-fatia-do-estado)
132
+ - [Com forma de estado complexa](#com-forma-de-estado-complexa)
133
+ - [Atualizar apenas o estado e sincronizar com a URL manualmente](#atualizar-apenas-o-estado-e-sincronizar-com-a-url-manualmente)
134
+ - [Outros hooks e helpers](#outros-hooks-e-helpers)
135
+ - [Hook `useUrlStateBase` para outros routers](#hook-useurlstatebase-para-outros-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` e `decodeState`](#helpers-encodestate-e-decodestate)
140
+ - [Helpers `encode` e `decode`](#helpers-encode-e-decode)
141
+ - [Boas práticas](#boas-práticas)
142
+ - [Pegadinhas](#pegadinhas)
143
+ - [Outros](#outros)
144
+ - [Contribuir e/ou executar localmente](#contribuir-eou-executar-localmente)
145
+ - [Roteiro](#roteiro)
146
+ - [Contato e suporte](#contato-e-suporte)
147
+ - [Changelog](#changelog)
148
+ - [Menções](#menções)
149
+ - [Licença](#licença)
150
+ - [Me contrate](#me-contrate)
151
+ - [Inspiração](#inspiração)
152
+
153
+ <!-- toc:end -->
154
+
155
+ ## Instalação
156
+
157
+ ### 1. Instalar o pacote
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 o tsconfig.json
169
+
170
+ No `tsconfig.json`, em `compilerOptions`, defina `"moduleResolution": "Bundler"`, ou `"moduleResolution": "Node16"`, ou `"moduleResolution": "NodeNext"`.
171
+ Possivelmente será necessário definir `"module": "ES2022"`, ou `"module": "ESNext"`
172
+
173
+ ## Uso com agentes de codificação de IA
174
+
175
+ O `state-in-url` inclui arquivos de habilidades (skills) para o [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview), para que os agentes de IA (Claude Code, Cursor, Copilot, Codex, etc.) carreguem os padrões corretos e evitem erros comuns ao usar a biblioteca. Após instalar o `state-in-url`, execute uma vez no seu projeto:
176
+
177
+ ```sh
178
+ npx @tanstack/intent@latest install
179
+ ```
180
+
181
+ Isso configura seu agente instalado para descobrir as habilidades em `node_modules/state-in-url/skills/`. Liste as habilidades disponíveis com `npx @tanstack/intent@latest list`.
182
+
183
+ ## useUrlState
184
+
185
+ Hook principal que recebe o estado inicial como parâmetro e retorna o objeto de estado, um callback para atualizar a url e um callback para atualizar apenas o estado.
186
+ Todos os componentes que usam o mesmo objeto `state` são sincronizados automaticamente.
187
+
188
+ ### Hook useUrlState para Next.js
189
+
190
+ [Documentação completa da API](packages/urlstate/next/useUrlState)
191
+
192
+ [Exemplo de React-Router](#hook-useurlstate-para-react-router)
193
+
194
+ #### Exemplos de uso
195
+
196
+ ##### Básico
197
+
198
+ 1. Defina a forma do estado com valores padrão
199
+
200
+ ```typescript
201
+ // userState.ts
202
+ // Somente os parâmetros com valor diferente do padrão irão para a url.
203
+ export const userState: UserState = { name: '', age: 0 }
204
+
205
+ // Use `Type`, não `Interface`!
206
+ type UserState = { name: string, age: number }
207
+ ```
208
+
209
+ 2. Importe e use
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
+ // pode passar o argumento `replace`, ele controla se `setUrl` usará `router.push` ou `router.replace`, por padrão replace=true
219
+ // pode passar `searchParams` de componentes de servidor, passe `useHistory: false` se precisar buscar algo no componente de servidor
220
+ const { urlState, setUrl, setState } = useUrlState(userState);
221
+
222
+ return (
223
+ <div>
224
+ // urlState.name retornará o valor padrão de `userState` se a url estiver vazia
225
+ <input value={urlState.name}
226
+ // mesma api que React.useState, por exemplo 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
+ // Pode atualizar o estado imediatamente, mas sincronizar a alteração com a url conforme necessário
236
+ onBlur={() => setUrl()}
237
+ />
238
+
239
+ <button onClick={() => setUrl((_, initial) => initial)}>
240
+ Reset
241
+ </button>
242
+
243
+ </div>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ##### Com renderização no servidor
249
+
250
+ <details>
251
+ <Summary>Exemplo</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
+ ##### Usando o hook no componente `layout`
274
+
275
+ <details>
276
+ <Summary>Exemplo</Summary>
277
+ Essa é uma parte complicada, já que o nextjs com app router não permite acessar searchParams do lado do servidor. Há uma solução alternativa usando middleware, mas não é bonita e pode parar de funcionar após uma atualização do nextjs.
278
+
279
+ ```typescript
280
+ // adicione ao `layout.tsx` apropriado
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
+ ##### Com forma de estado arbitrária (não recomendado)
325
+
326
+ <details>
327
+ <Summary>Exemplo</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
+ A API é a mesma da versão para Next.js, exceto que você pode passar opções do tipo [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
345
+
346
+ [Documentação da API](packages/urlstate/remix/useUrlState)
347
+
348
+ #### Exemplo
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
+ // Pode atualizar o estado imediatamente, mas sincronizar a alteração com a url conforme necessário
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
+ [Exemplo de código](packages/example-remix2/app/routes/Form-for-test.tsx)
428
+
429
+ ### Hook useUrlState para React-Router
430
+
431
+ A API é a mesma da versão para Next.js, exceto que você pode passar opções do tipo [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99).
432
+
433
+ [Documentação da API](packages/urlstate/react-router/useUrlState)
434
+
435
+ #### Exemplo
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
+ // Pode atualizar o estado imediatamente, mas sincronizar a alteração com a url conforme necessário
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
+ [Exemplo de código](packages/example-react-router6/src/Form-for-test.tsx)
517
+
518
+ ## Receitas
519
+ ##### Hook personalizado para trabalhar convenientemente com uma fatia do estado
520
+ <details>
521
+ <Summary>Exemplo</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
+ // a primeira navegação adicionará uma nova entrada ao histórico
549
+ // todas as seguintes apenas substituirão essa entrada
550
+ // assim, o histórico terá apenas 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
+ ##### Com forma de estado complexa
569
+
570
+ <details>
571
+ <Summary>Exemplo</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` será inferido do 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
+ [Exemplo de código da página de demonstração](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
646
+ </details>
647
+
648
+ ##### Atualizar apenas o estado e sincronizar com a URL manualmente
649
+
650
+ <details>
651
+ <Summary>Exemplo</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á o estado por conteúdo e não por referência, e disparará a atualização apenas para novos valores
660
+ setUrl(urlState);
661
+ }, 500);
662
+
663
+ return () => {
664
+ clearTimeout(timer.current);
665
+ };
666
+ }, [urlState, setUrl]);
667
+ ```
668
+
669
+ Sincronizar o estado em `onBlur` estará mais alinhado com o uso no mundo real.
670
+
671
+ ```typescript
672
+ <input onBlur={() => updateUrl()} .../>
673
+ ```
674
+
675
+ </details>
676
+
677
+ ## Outros hooks e helpers
678
+
679
+ ### Hook `useUrlStateBase` para outros routers
680
+
681
+ Hooks para criar seus próprios hooks `useUrlState` com outros routers, por exemplo react-router ou tanstack router.
682
+
683
+ [Documentação da API](packages/urlstate/useUrlStateBase)
684
+
685
+ ### Hook `useSharedState` para React.js
686
+
687
+ Hook para compartilhar estado entre quaisquer componentes React, testado com Next.js e 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
+ [Documentação da API](packages/urlstate/useSharedState/README.pt-BR.md)
701
+
702
+ ### Hook `useLinkProps` para React.js
703
+
704
+ Hook para levar o estado até um link que aponta para outra rota, por exemplo um seletor de idioma. `setUrl` sempre escreve no caminho atual; este não.
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
+ A marcação mantém o `href` simples, então rastreadores e `hreflang` enxergam a URL canônica; o estado é lido no clique.
722
+
723
+ [Documentação da API](packages/urlstate/useLinkProps/README.pt-BR.md)
724
+
725
+ ### Hook `useUrlEncode` para React.js
726
+
727
+ [Documentação da API](packages/urlstate/useUrlEncode/README.pt-BR.md)
728
+
729
+ ### Helpers `encodeState` e `decodeState`
730
+
731
+ [Documentação da API](packages/urlstate/encodeState/README.pt-BR.md)
732
+
733
+ ### Helpers `encode` e `decode`
734
+
735
+ [Documentação da API](packages/urlstate/encoder/README.pt-BR.md)
736
+
737
+ ## Boas práticas
738
+
739
+ - Defina a forma do seu estado como uma constante
740
+ - Use TypeScript para maior segurança de tipos e autocompletar
741
+ - Evite armazenar informações sensíveis nos parâmetros da URL (SSN, chaves de API, etc.)
742
+ - Use esta [extensão](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) para erros de TS legíveis
743
+
744
+ Você pode criar hooks de estado para fatias do estado e reutilizá-los em toda a aplicação. Por exemplo:
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
+ // outra lógica
761
+
762
+ // redefinir os parâmetros de consulta ao navegar para outra página
763
+ React.useEffect(() => {
764
+ return reset
765
+ }, [])
766
+
767
+ return { userState: urlState, setUserState: setUrl };;
768
+ }
769
+
770
+ ```
771
+
772
+ ## Pegadinhas
773
+
774
+ 1. Só é possível passar valores serializáveis; `Function`, `BigInt` ou `Symbol` não funcionarão, e provavelmente coisas como `ArrayBuffer` também não. Tudo o que pode ser serializado para JSON funcionará.
775
+ 2. Os servidores da Vercel limitam o tamanho dos cabeçalhos (string de consulta e outras coisas) a **14KB**, então mantenha o estado da sua URL abaixo de ~5000 palavras. <https://vercel.com/docs/errors/URL_TOO_LONG>
776
+ 3. Testado com `next.js` 14/15/16 com app router; sem planos de suportar pages.
777
+
778
+ ## Outros
779
+
780
+ ### Contribuir e/ou executar localmente
781
+
782
+ Veja o [documento de contribuição](CONTRIBUTING.md)
783
+
784
+ ## Roteiro
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 armazenar estado no hash ?
792
+
793
+ ## Contato e suporte
794
+
795
+ - Crie uma [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) para relatórios de bugs, solicitações de recursos ou perguntas
796
+
797
+ ## [Changelog](CHANGELOG.md)
798
+
799
+ ## Menções
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
+ ## Licença
806
+
807
+ Este projeto está licenciado sob a [licença MIT](LICENSE).
808
+
809
+ ## Me contrate
810
+
811
+ Sou **Aleksandr Smyshliaev** — único autor e mantenedor desta biblioteca.
812
+ Engenheiro frontend sênior (React / Next.js / TypeScript, mais de 8 anos) e **disponível para trabalho remoto em tempo integral agora mesmo**.
813
+
814
+ Esta biblioteca é a versão resumida do que eu faço bem: uma API tipada sobre um primitivo de navegador bagunçado, zero dependências e estabilidade entre Next.js, Remix e React Router ao longo de várias versões maiores do React.
815
+
816
+ - **Melhor em** — bibliotecas de componentes, gerenciamento de estado e suítes de teste que sobrevivem a um refactor.
817
+ - **Também são meus** —
818
+ [react-horizontal-scrolling-menu](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu)
819
+ (~84k instalações semanais),
820
+ [test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
821
+ (gravação/reprodução para Playwright),
822
+ [llm-queue](https://github.com/asmyshlyaev177/llm-queue).
823
+ - **Onde** — Tbilisi, Geórgia (GMT+4), sobreposição total com CET. Entidade contratante registrada, então o engajamento B2B não precisa de configuração de employer-of-record.
824
+ - **Fale comigo** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
825
+ [asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
826
+ [LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
827
+
828
+ ## Inspiração
829
+
830
+ [NUQS](https://github.com/47ng/nuqs)
831
+
832
+ [Usando a URL para armazenar estado no Vue](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
833
+
834
+ [Armazenando estado na URL](https://antonz.org/storing-state/)
835
+
836
+ [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)