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.
- 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/encoder/encoder.d.ts +1 -1
- package/dist/encoder/encoder.js +1 -1
- package/dist/encoder/encoder.mjs +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/next/useUrlState/useUrlState.d.ts +3 -3
- package/dist/next/useUrlState/useUrlState.js +1 -1
- package/dist/next/useUrlState/useUrlState.mjs +1 -1
- package/dist/package.json +34 -13
- package/dist/react-router/useUrlState/useUrlState.js +1 -1
- package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
- package/dist/react-router6/useUrlState/useUrlState.js +1 -1
- package/dist/useInsertionEffect.js +1 -1
- 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/useSharedState/useSharedState.d.ts +1 -1
- package/dist/useSharedState/useSharedState.js +1 -1
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlEncode/useUrlEncode.js +1 -1
- package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
- 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 +12 -0
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +34 -13
- package/skills/feature-state-hook/SKILL.md +36 -9
- package/skills/form-library-integration/SKILL.md +3 -4
- package/skills/input-handling/SKILL.md +20 -6
- package/skills/nextjs-ssr/SKILL.md +77 -9
- package/skills/react-router-remix-setup/SKILL.md +1 -1
- package/skills/shared-state-no-url/SKILL.md +1 -1
package/README.pt-BR.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](./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
|
+
[](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">Não hesite em abrir uma issue se encontrou um bug ou para solicitar recursos</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">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)
|