state-in-url 6.4.0 → 6.5.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 (53) hide show
  1. package/README.es.md +146 -4
  2. package/README.fr.md +146 -4
  3. package/README.ja.md +146 -4
  4. package/README.ko.md +146 -4
  5. package/README.md +144 -3
  6. package/README.pt-BR.md +146 -4
  7. package/README.ru.md +146 -4
  8. package/README.vi.md +146 -4
  9. package/README.zh-CN.md +146 -4
  10. package/dist/astro/index.d.ts +1 -0
  11. package/dist/astro/index.js +1 -0
  12. package/dist/astro/index.mjs +1 -0
  13. package/dist/astro/useUrlState/index.d.ts +1 -0
  14. package/dist/astro/useUrlState/index.js +1 -0
  15. package/dist/astro/useUrlState/index.mjs +1 -0
  16. package/dist/astro/useUrlState/useUrlState.d.ts +85 -0
  17. package/dist/astro/useUrlState/useUrlState.js +1 -0
  18. package/dist/astro/useUrlState/useUrlState.mjs +1 -0
  19. package/dist/encodeState/encodeState.d.ts +1 -1
  20. package/dist/encodeState/encodeState.js +1 -1
  21. package/dist/encodeState/encodeState.mjs +1 -1
  22. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  23. package/dist/next/useUrlState/useUrlState.js +1 -1
  24. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  25. package/dist/package.json +89 -39
  26. package/dist/react-router/useUrlState/useUrlState.d.ts +5 -5
  27. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  28. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  29. package/dist/react-router6/useUrlState/useUrlState.d.ts +7 -7
  30. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  31. package/dist/remix/useUrlState/useUrlState.d.ts +5 -5
  32. package/dist/useIsomorphicLayoutEffect.d.ts +2 -0
  33. package/dist/useIsomorphicLayoutEffect.js +1 -0
  34. package/dist/useIsomorphicLayoutEffect.mjs +1 -0
  35. package/dist/useLinkProps/useLinkProps.mjs +1 -1
  36. package/dist/useSharedState/useSharedState.js +1 -1
  37. package/dist/useSharedState/useSharedState.mjs +1 -1
  38. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  39. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  40. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  41. package/dist/utils.js +1 -1
  42. package/dist/utils.mjs +1 -1
  43. package/package.json +89 -39
  44. package/skills/astro-setup/SKILL.md +204 -0
  45. package/skills/feature-state-hook/SKILL.md +1 -1
  46. package/skills/form-library-integration/SKILL.md +1 -1
  47. package/skills/input-handling/SKILL.md +1 -1
  48. package/skills/nextjs-ssr/SKILL.md +1 -1
  49. package/skills/react-router-remix-setup/SKILL.md +1 -1
  50. package/skills/shared-state-no-url/SKILL.md +1 -1
  51. package/dist/useInsertionEffect.d.ts +0 -2
  52. package/dist/useInsertionEffect.js +0 -1
  53. package/dist/useInsertionEffect.mjs +0 -1
package/README.ko.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · 한국어 · [Русский](./README.ru.md) · [Español](./README.es.md) · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
3
- <!-- i18n:meta locale=ko source=README.md source-blob=bbf45a10aae9e88f68b27d84f51ee3e7e8a1e436 status=translated -->
3
+ <!-- i18n:meta locale=ko source=README.md source-blob=4d98ba970e6aab8f29987fbcba29fb6c61c2fc67 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@
69
69
  쿼리 매개변수에 모든 사용자 상태를 저장하세요. 브라우저 URL에 JSON이 있다고 상상해보세요. 데이터의 타입과 구조를 유지하면서 모든 것을 저장할 수 있습니다. 예를 들어 숫자는 문자열이 아닌 숫자로, 날짜는 날짜로 디코딩되며, 객체와 배열도 지원됩니다.
70
70
  매우 간단하고 빠르며 정적 Typescript 유효성 검사를 제공합니다. 딥 링크, 즉 URL 동기화가 쉬워집니다.
71
71
 
72
- Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬퍼를 포함합니다.
72
+ Next.js, react-router, Remix, Astro용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬퍼를 포함합니다.
73
73
  최신 브라우저는 거대한 URL을 지원하고 사용자는 쿼리 문자열을 신경 쓰지 않습니다(전체 선택 후 복사/붙여넣기 워크플로우입니다).
74
74
 
75
75
  쿼리 문자열을 원래 의도대로 상태 관리에 사용할 때입니다.
@@ -98,10 +98,32 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
98
98
  - **서버 사이드 렌더링**: 서버 컴포넌트에서 사용 가능, Next.js 14 및 15 지원
99
99
  - **경량**: 의존성 없음, 라이브러리 크기 2KB 미만
100
100
  - **개발자 경험**: 좋은 개발자 경험, 문서, JSDoc 주석 및 예제
101
- - **프레임워크 유연성**: `Next.js` 및 `react-router`용 훅, 다른 프레임워크나 순수 JS와 함께 사용할 수 있는 헬퍼
101
+ - **프레임워크 유연성**: `Next.js`, `react-router`, `Remix`, `Astro`용 훅, 다른 프레임워크나 순수 JS와 함께 사용할 수 있는 헬퍼
102
102
  - **충분한 테스트**: [Chrome/Firefox/Safari용 단위 테스트 및 Playwright 테스트](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
103
  - **자유로운 라이선스**: MIT
104
104
 
105
+ ### nuqs와 비교
106
+
107
+ [nuqs](https://github.com/47ng/nuqs) 대안을 찾고 계신가요? 둘 다 타입이 유지되는 상태를 쿼리 문자열에 저장하지만, 필요한 설정량과 값으로 담을 수 있는 것이 다릅니다.
108
+
109
+ | 항목 | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | 설정 | 불필요 — hook을 import하면 끝 | 어댑터 컴포넌트로 앱을 감싸야 함 |
112
+ | 상태 형태 | `React.useState` 같은 타입 객체 하나 | 키별 값, 각 키마다 파서 선언 |
113
+ | 컴포넌트 간 재사용 | hook을 한 번 감싸면 — 모든 컴포넌트가 상태 공유, props 불필요 | 파서 맵을 감싸는 hook은 직접 추출 |
114
+ | 중첩 객체와 배열 | 기본 지원 — 구조와 타입 유지 | JSON 파서에 자체 런타임 검증 추가 필요 |
115
+ | 날짜 | 자동으로 유지 | 내장 파서를 키마다 선언 |
116
+ | 크기, 전체 import | 약 2.9 KB gzip | 약 6.7 KB gzip |
117
+ | 런타임 의존성 | 없음 | 1개 |
118
+ | 라우터 | Next.js, React Router v6/v7, Remix, Astro, 순수 JS 헬퍼 | Next.js, React Router, Remix, TanStack Router, 순수 React |
119
+
120
+ 크기: 라이브러리 전체 import, esbuild minify + gzip, 2026년 8월 nuqs 2.10.1 기준 측정.
121
+
122
+ nuqs도 훌륭한 라이브러리입니다. 값마다 읽기 쉬운 쿼리 파라미터를 원하거나 TanStack Router를 쓴다면 nuqs를, 타입 객체 전체를 설정 없이 URL에 넣고 싶다면 state-in-url을 선택하세요.
123
+
124
+
125
+ 전체 비교 — 같은 기능의 두 구현, 다른 대안(TanStack Router, use-query-params), 마이그레이션 노트 — 는 <https://state-in-url.dev/vs/nuqs> 에 있습니다.
126
+
105
127
  ## 목차
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
111
133
  - [왜 `state-in-url`을 사용해야 하나요?](#왜-state-in-url을-사용해야-하나요)
112
134
  - [사용 사례](#사용-사례)
113
135
  - [특징](#특징)
136
+ - [nuqs와 비교](#nuqs와-비교)
114
137
  - [목차](#목차)
115
138
  - [설치](#설치)
116
139
  - [1. 패키지 설치](#1-패키지-설치)
@@ -127,6 +150,8 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
127
150
  - [예제](#예제)
128
151
  - [React-Router용 useUrlState 훅](#react-router용-useurlstate-훅)
129
152
  - [예제](#예제-1)
153
+ - [Astro용 useUrlState 훅](#astro용-useurlstate-훅)
154
+ - [예제](#예제-2)
130
155
  - [레시피](#레시피)
131
156
  - [상태의 일부를 편리하게 다루기 위한 커스텀 훅](#상태의-일부를-편리하게-다루기-위한-커스텀-훅)
132
157
  - [복잡한 상태 구조 사용](#복잡한-상태-구조-사용)
@@ -511,6 +536,123 @@ const tags = [
511
536
 
512
537
  [예제 코드](packages/example-react-router6/src/Form-for-test.tsx)
513
538
 
539
+ ### Astro용 useUrlState 훅
540
+
541
+ React 아일랜드용입니다. Astro에는 기본적으로 클라이언트 측 라우터가 없으므로, 이 훅은 `window.history`로 URL을 쓰고, 뒤로/앞으로 이동과 그 외 모든 `pushState`/`replaceState` 시점에 (Astro 자체의 `<ClientRouter />` 포함) URL을 다시 읽어 옵니다. 한 페이지의 아일랜드들은 감싸는 것 없이 상태를 공유합니다.
542
+
543
+ [API 문서](packages/urlstate/astro/useUrlState)
544
+
545
+ #### 예제
546
+
547
+ ```typescript
548
+ // src/state.ts
549
+ export const form: Form = {
550
+ name: '',
551
+ age: undefined,
552
+ agree_to_terms: false,
553
+ tags: [],
554
+ };
555
+
556
+ type Form = {
557
+ name: string;
558
+ age?: number;
559
+ agree_to_terms: boolean;
560
+ tags: { id: string; value: { text: string; time: Date } }[];
561
+ };
562
+ ```
563
+
564
+ 페이지는 온디맨드로 렌더링되어야 합니다(어댑터와 함께 `output: 'server'`를 쓰거나, 해당 페이지에 `export const prerender = false`를 선언). 프리렌더링된 페이지에는 요청이 없으므로 아일랜드는 `{}`를 받고 하이드레이션 후에 URL을 읽습니다.
565
+
566
+ ```astro
567
+ ---
568
+ // src/pages/index.astro
569
+ import { Form } from '../components/Form';
570
+ import { Status } from '../components/Status';
571
+
572
+ // 서버 렌더링이 URL과 일치하므로 하이드레이션이 바로잡을 것이 없습니다.
573
+ // 일반 객체여야 합니다. 아일랜드 props는 직렬화되지만 URLSearchParams는 직렬화되지 않습니다.
574
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
575
+ ---
576
+
577
+ <Form client:load searchParams={searchParams} />
578
+ <Status client:load searchParams={searchParams} />
579
+ ```
580
+
581
+ ```typescript
582
+ // src/components/Form.tsx
583
+ import React from 'react';
584
+ import { useUrlState } from 'state-in-url/astro';
585
+
586
+ import { form } from '../state';
587
+
588
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
589
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
590
+
591
+ const onChangeTags = React.useCallback(
592
+ (tag: (typeof tags)[number]) => {
593
+ setUrl((curr) => ({
594
+ ...curr,
595
+ tags: curr.tags.find((t) => t.id === tag.id)
596
+ ? curr.tags.filter((t) => t.id !== tag.id)
597
+ : curr.tags.concat(tag),
598
+ }));
599
+ },
600
+ [setUrl],
601
+ );
602
+
603
+ return (
604
+ <div>
605
+ {tags.map((tag) => (
606
+ <Tag
607
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
608
+ text={tag.value.text}
609
+ onClick={() => onChangeTags(tag)}
610
+ key={tag.id}
611
+ />
612
+ ))}
613
+
614
+ <input value={urlState.name}
615
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
616
+ // 상태를 즉시 업데이트하되 필요에 따라 URL에 동기화
617
+ onBlur={() => setUrl()}
618
+ />
619
+ </div>
620
+ );
621
+ }
622
+
623
+ const tags = [
624
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
625
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
626
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
627
+ ];
628
+
629
+ // 두 번째 아일랜드인 Status.tsx는 같은 상태를 읽습니다
630
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
631
+ const { urlState } = useUrlState(form, { searchParams });
632
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
633
+ }
634
+ ```
635
+
636
+ Preact 아일랜드도 같은 방식으로 동작합니다. `@astrojs/preact`와 `compat: true`를 쓰면 서버 빌드와 클라이언트 빌드 모두에서 `react`가 `preact/compat`으로 해석되므로, 위의 import는 그대로입니다.
637
+
638
+ 아일랜드 없이, 클라이언트 프레임워크가 전혀 없는 페이지에서는 같은 상태가 [`decodeState`와 `encodeState`](#encodestate-및-decodestate-헬퍼)를 통해 프론트매터에 존재합니다:
639
+
640
+ ```astro
641
+ ---
642
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
643
+
644
+ import { form } from '../state';
645
+
646
+ const state = decodeState(Astro.url.searchParams, form);
647
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
648
+ ---
649
+
650
+ <pre>{JSON.stringify(state)}</pre>
651
+ <a href={`?${withName}`}>Alice</a>
652
+ ```
653
+
654
+ [예제 코드](packages/example-astro/src/components/Form-for-test.tsx), [순수 Astro 페이지](packages/example-astro/src/pages/pure-astro.astro)
655
+
514
656
  ## 레시피
515
657
  ##### 상태의 일부를 편리하게 다루기 위한 커스텀 훅
516
658
  <details>
@@ -782,7 +924,7 @@ export const useUserState = () => {
782
924
  - [x] `react-router`용 훅
783
925
  - [x] `remix`용 훅
784
926
  - [ ] `svelte`용 훅
785
- - [ ] `astro`용 훅
927
+ - [x] `astro`용 훅
786
928
  - [ ] 해시에 상태를 저장하는 훅?
787
929
 
788
930
  ## 문의 및 지원
package/README.md CHANGED
@@ -69,7 +69,7 @@ Share if it useful for you.
69
69
  Store any user state in query parameters; imagine JSON in a browser URL. All of it with keeping types and structure of data, e.g. numbers will be decoded as numbers not strings, dates as dates, etc, objects and arrays supported.
70
70
  Dead simple, fast, and with static Typescript validation. Deep links, aka URL synchronization, made easy.
71
71
 
72
- Contains `useUrlState` hook for Next.js and react-router, and helpers for anything else on JS.
72
+ Contains `useUrlState` hook for Next.js, react-router, Remix and Astro, and helpers for anything else on JS.
73
73
  Since modern browsers support huge URLs and users don't care about query strings (it is a select all and copy/past workflow).
74
74
 
75
75
  Time to use query string for state management, as it was originally intended.
@@ -98,10 +98,31 @@ This library is a good alternative for NUQS.
98
98
  - **Server Side Rendering**: Can use it in Server Components, Next.js 14, 15, and 16 are supported
99
99
  - **Lightweight**: Zero dependencies, library less than 2KB
100
100
  - **DX**: Good developer experience, documentation, JSDoc comments, and examples
101
- - **Framework Flexibility**: Hooks for `Next.js` and `react-router`, helpers to use it with other frameworks or pure JS
101
+ - **Framework Flexibility**: Hooks for `Next.js`, `react-router`, `Remix` and `Astro`, helpers to use it with other frameworks or pure JS
102
102
  - **Well tested**: [Unit tests and Playwright tests for Chrome/Firefox/Safari](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
103
  - **Permissive license**: MIT
104
104
 
105
+ ### Comparison with nuqs
106
+
107
+ Searching for a [nuqs](https://github.com/47ng/nuqs) alternative? Both keep typed state in the query string; they differ in how much you set up and what a value can be.
108
+
109
+ | What | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | Setup | None — import the hook and go | Adapter component wraps the app |
112
+ | State shape | One typed object, like `React.useState` | Per-key values, a parser declared for each |
113
+ | Reuse across components | Wrap the hook once — every component shares the state, no props | Extract your own hook around the parser map |
114
+ | Nested objects and arrays | Built in — structure and types preserved | JSON parser plus your own runtime validator |
115
+ | Dates | Preserved automatically | Built-in parser, declared per key |
116
+ | Size, full import | ~2.9 KB gzipped | ~6.7 KB gzipped |
117
+ | Runtime dependencies | None | One |
118
+ | Routers | Next.js, React Router v6/v7, Remix, Astro, plain JS helpers | Next.js, React Router, Remix, TanStack Router, plain React |
119
+
120
+ Sizes: whole-library import, esbuild minify + gzip, measured August 2026 against nuqs 2.10.1.
121
+
122
+ nuqs is a fine library — reach for it when you want each value as its own readable query param, or you are on TanStack Router. Reach for state-in-url when you want a whole typed object in the URL with zero setup.
123
+
124
+ The full comparison — the same feature built in both, other alternatives (TanStack Router, use-query-params) and migration notes — lives at <https://state-in-url.dev/vs/nuqs>.
125
+
105
126
  ## Table of content
106
127
 
107
128
  <!-- toc:start -->
@@ -111,6 +132,7 @@ This library is a good alternative for NUQS.
111
132
  - [Why use `state-in-url`?](#why-use-state-in-url)
112
133
  - [Use cases](#use-cases)
113
134
  - [Features](#features)
135
+ - [Comparison with nuqs](#comparison-with-nuqs)
114
136
  - [Table of content](#table-of-content)
115
137
  - [installation](#installation)
116
138
  - [1. Install package](#1-install-package)
@@ -127,6 +149,8 @@ This library is a good alternative for NUQS.
127
149
  - [Example](#example)
128
150
  - [useUrlState hook for React-Router](#useurlstate-hook-for-react-router)
129
151
  - [Example](#example-1)
152
+ - [useUrlState hook for Astro](#useurlstate-hook-for-astro)
153
+ - [Example](#example-2)
130
154
  - [Recipes](#recipes)
131
155
  - [Custom hook to work with slice of state conveniently](#custom-hook-to-work-with-slice-of-state-conveniently)
132
156
  - [With complex state shape](#with-complex-state-shape)
@@ -515,6 +539,123 @@ const tags = [
515
539
 
516
540
  [Example code](packages/example-react-router6/src/Form-for-test.tsx)
517
541
 
542
+ ### useUrlState hook for Astro
543
+
544
+ For React islands. Astro has no client-side router by default, so the hook writes the URL with `window.history` and reads it back on back/forward and on any other `pushState`/`replaceState`, Astro's own `<ClientRouter />` included. Islands on a page share the state, with nothing to wrap them in.
545
+
546
+ [API Docs](packages/urlstate/astro/useUrlState)
547
+
548
+ #### Example
549
+
550
+ ```typescript
551
+ // src/state.ts
552
+ export const form: Form = {
553
+ name: '',
554
+ age: undefined,
555
+ agree_to_terms: false,
556
+ tags: [],
557
+ };
558
+
559
+ type Form = {
560
+ name: string;
561
+ age?: number;
562
+ agree_to_terms: boolean;
563
+ tags: { id: string; value: { text: string; time: Date } }[];
564
+ };
565
+ ```
566
+
567
+ The page must be rendered on demand (`output: 'server'`, or `export const prerender = false` on the page, with an adapter): a prerendered page has no request, so the island gets `{}` and reads the URL after hydration.
568
+
569
+ ```astro
570
+ ---
571
+ // src/pages/index.astro
572
+ import { Form } from '../components/Form';
573
+ import { Status } from '../components/Status';
574
+
575
+ // The server render matches the URL, so hydration has nothing to correct.
576
+ // A plain object: island props are serialized, URLSearchParams is not.
577
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
578
+ ---
579
+
580
+ <Form client:load searchParams={searchParams} />
581
+ <Status client:load searchParams={searchParams} />
582
+ ```
583
+
584
+ ```typescript
585
+ // src/components/Form.tsx
586
+ import React from 'react';
587
+ import { useUrlState } from 'state-in-url/astro';
588
+
589
+ import { form } from '../state';
590
+
591
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
592
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
593
+
594
+ const onChangeTags = React.useCallback(
595
+ (tag: (typeof tags)[number]) => {
596
+ setUrl((curr) => ({
597
+ ...curr,
598
+ tags: curr.tags.find((t) => t.id === tag.id)
599
+ ? curr.tags.filter((t) => t.id !== tag.id)
600
+ : curr.tags.concat(tag),
601
+ }));
602
+ },
603
+ [setUrl],
604
+ );
605
+
606
+ return (
607
+ <div>
608
+ {tags.map((tag) => (
609
+ <Tag
610
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
611
+ text={tag.value.text}
612
+ onClick={() => onChangeTags(tag)}
613
+ key={tag.id}
614
+ />
615
+ ))}
616
+
617
+ <input value={urlState.name}
618
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
619
+ // Can update state immediately but sync change to url as needed
620
+ onBlur={() => setUrl()}
621
+ />
622
+ </div>
623
+ );
624
+ }
625
+
626
+ const tags = [
627
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
628
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
629
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
630
+ ];
631
+
632
+ // Status.tsx, a second island, reads the same state
633
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
634
+ const { urlState } = useUrlState(form, { searchParams });
635
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
636
+ }
637
+ ```
638
+
639
+ Preact islands work the same way: with `@astrojs/preact` and `compat: true`, `react` resolves to `preact/compat` in both the server and the client build, and the import above is unchanged.
640
+
641
+ Without islands, on a page with no client framework at all, the same state lives in the frontmatter through [`decodeState` and `encodeState`](#encodestate-and-decodestate-helpers):
642
+
643
+ ```astro
644
+ ---
645
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
646
+
647
+ import { form } from '../state';
648
+
649
+ const state = decodeState(Astro.url.searchParams, form);
650
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
651
+ ---
652
+
653
+ <pre>{JSON.stringify(state)}</pre>
654
+ <a href={`?${withName}`}>Alice</a>
655
+ ```
656
+
657
+ [Example code](packages/example-astro/src/components/Form-for-test.tsx), [pure Astro page](packages/example-astro/src/pages/pure-astro.astro)
658
+
518
659
  ## Recipes
519
660
  ##### Custom hook to work with slice of state conveniently
520
661
  <details>
@@ -787,7 +928,7 @@ See [Contributing doc](CONTRIBUTING.md)
787
928
  - [x] hook for `react-router`
788
929
  - [x] hook for `remix`
789
930
  - [ ] hook for `svelte`
790
- - [ ] hook for `astro`
931
+ - [x] hook for `astro`
791
932
  - [ ] hook for store state in hash ?
792
933
 
793
934
  ## Contact & Support
package/README.pt-BR.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md) · Português (BR) · [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 -->
3
+ <!-- i18n:meta locale=pt-BR source=README.md source-blob=4d98ba970e6aab8f29987fbcba29fb6c61c2fc67 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@ Compartilhe se for útil para você.
69
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
70
  Extremamente simples, rápido e com validação estática de TypeScript. Links profundos, ou seja, sincronização de URL, facilitados.
71
71
 
72
- Contém o hook `useUrlState` para Next.js e react-router, e helpers para qualquer outra coisa em JS.
72
+ Contém o hook `useUrlState` para Next.js, react-router, Remix e Astro, e helpers para qualquer outra coisa em JS.
73
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
74
 
75
75
  É hora de usar a string de consulta para gerenciamento de estado, como foi originalmente planejado.
@@ -98,10 +98,32 @@ Esta biblioteca é uma boa alternativa ao NUQS.
98
98
  - **Renderização no servidor**: Pode ser usado em componentes de servidor, Next.js 14, 15 e 16 são suportados
99
99
  - **Leve**: Zero dependências, biblioteca com menos de 2KB
100
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
101
+ - **Flexibilidade de frameworks**: Hooks para `Next.js`, `react-router`, `Remix` e `Astro`, helpers para usar com outros frameworks ou JS puro
102
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
103
  - **Licença permissiva**: MIT
104
104
 
105
+ ### Comparação com o nuqs
106
+
107
+ Procurando uma alternativa ao [nuqs](https://github.com/47ng/nuqs)? Ambas guardam estado tipado na query string; diferem em quanta configuração exigem e no que um valor pode ser.
108
+
109
+ | O quê | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | Configuração | Nenhuma — importe o hook e pronto | Um componente adaptador envolve o app |
112
+ | Formato do estado | Um objeto tipado, como `React.useState` | Valores por chave, com um parser declarado para cada uma |
113
+ | Reuso entre componentes | Envolva o hook uma vez — cada componente compartilha o estado, sem props | Você extrai seu próprio hook em volta do mapa de parsers |
114
+ | Objetos e arrays aninhados | Nativo — estrutura e tipos preservados | Parser JSON mais seu próprio validador |
115
+ | Datas | Preservadas automaticamente | Parser embutido, declarado por chave |
116
+ | Tamanho, import completo | ~2,9 KB gzip | ~6,7 KB gzip |
117
+ | Dependências em runtime | Nenhuma | Uma |
118
+ | Roteadores | Next.js, React Router v6/v7, Remix, Astro, helpers para JS puro | Next.js, React Router, Remix, TanStack Router, React puro |
119
+
120
+ Tamanhos: import da biblioteca inteira, esbuild minify + gzip, medido em agosto de 2026 contra o nuqs 2.10.1.
121
+
122
+ nuqs é uma boa biblioteca — prefira-o se quiser cada valor como seu próprio query param legível, ou se usa TanStack Router. Prefira state-in-url quando quiser um objeto tipado inteiro na URL sem configuração.
123
+
124
+
125
+ A comparação completa — a mesma feature nas duas, outras alternativas (TanStack Router, use-query-params) e notas de migração — está em <https://state-in-url.dev/vs/nuqs>.
126
+
105
127
  ## Índice
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Esta biblioteca é uma boa alternativa ao NUQS.
111
133
  - [Por que usar `state-in-url`?](#por-que-usar-state-in-url)
112
134
  - [Casos de uso](#casos-de-uso)
113
135
  - [Recursos](#recursos)
136
+ - [Comparação com o nuqs](#comparação-com-o-nuqs)
114
137
  - [Índice](#índice)
115
138
  - [Instalação](#instalação)
116
139
  - [1. Instalar o pacote](#1-instalar-o-pacote)
@@ -127,6 +150,8 @@ Esta biblioteca é uma boa alternativa ao NUQS.
127
150
  - [Exemplo](#exemplo)
128
151
  - [Hook useUrlState para React-Router](#hook-useurlstate-para-react-router)
129
152
  - [Exemplo](#exemplo-1)
153
+ - [Hook useUrlState para Astro](#hook-useurlstate-para-astro)
154
+ - [Exemplo](#exemplo-2)
130
155
  - [Receitas](#receitas)
131
156
  - [Hook personalizado para trabalhar convenientemente com uma fatia do estado](#hook-personalizado-para-trabalhar-convenientemente-com-uma-fatia-do-estado)
132
157
  - [Com forma de estado complexa](#com-forma-de-estado-complexa)
@@ -515,6 +540,123 @@ const tags = [
515
540
 
516
541
  [Exemplo de código](packages/example-react-router6/src/Form-for-test.tsx)
517
542
 
543
+ ### Hook useUrlState para Astro
544
+
545
+ Para ilhas React. O Astro não tem um router no lado do cliente por padrão, então o hook escreve a URL com `window.history` e a lê de volta ao navegar para trás/para frente e em qualquer outro `pushState`/`replaceState`, incluindo o próprio `<ClientRouter />` do Astro. As ilhas de uma página compartilham o estado, sem precisar envolvê-las em nada.
546
+
547
+ [Documentação da API](packages/urlstate/astro/useUrlState)
548
+
549
+ #### Exemplo
550
+
551
+ ```typescript
552
+ // src/state.ts
553
+ export const form: Form = {
554
+ name: '',
555
+ age: undefined,
556
+ agree_to_terms: false,
557
+ tags: [],
558
+ };
559
+
560
+ type Form = {
561
+ name: string;
562
+ age?: number;
563
+ agree_to_terms: boolean;
564
+ tags: { id: string; value: { text: string; time: Date } }[];
565
+ };
566
+ ```
567
+
568
+ A página precisa ser renderizada sob demanda (`output: 'server'`, ou `export const prerender = false` na página, com um adapter): uma página pré-renderizada não tem requisição, então a ilha recebe `{}` e lê a URL após a hidratação.
569
+
570
+ ```astro
571
+ ---
572
+ // src/pages/index.astro
573
+ import { Form } from '../components/Form';
574
+ import { Status } from '../components/Status';
575
+
576
+ // A renderização no servidor corresponde à URL, então a hidratação não tem nada a corrigir.
577
+ // Um objeto simples: as props de ilhas são serializadas, URLSearchParams não.
578
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
579
+ ---
580
+
581
+ <Form client:load searchParams={searchParams} />
582
+ <Status client:load searchParams={searchParams} />
583
+ ```
584
+
585
+ ```typescript
586
+ // src/components/Form.tsx
587
+ import React from 'react';
588
+ import { useUrlState } from 'state-in-url/astro';
589
+
590
+ import { form } from '../state';
591
+
592
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
593
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
594
+
595
+ const onChangeTags = React.useCallback(
596
+ (tag: (typeof tags)[number]) => {
597
+ setUrl((curr) => ({
598
+ ...curr,
599
+ tags: curr.tags.find((t) => t.id === tag.id)
600
+ ? curr.tags.filter((t) => t.id !== tag.id)
601
+ : curr.tags.concat(tag),
602
+ }));
603
+ },
604
+ [setUrl],
605
+ );
606
+
607
+ return (
608
+ <div>
609
+ {tags.map((tag) => (
610
+ <Tag
611
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
612
+ text={tag.value.text}
613
+ onClick={() => onChangeTags(tag)}
614
+ key={tag.id}
615
+ />
616
+ ))}
617
+
618
+ <input value={urlState.name}
619
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
620
+ // Pode atualizar o estado imediatamente, mas sincronizar a alteração com a url conforme necessário
621
+ onBlur={() => setUrl()}
622
+ />
623
+ </div>
624
+ );
625
+ }
626
+
627
+ const tags = [
628
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
629
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
630
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
631
+ ];
632
+
633
+ // Status.tsx, uma segunda ilha, lê o mesmo estado
634
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
635
+ const { urlState } = useUrlState(form, { searchParams });
636
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
637
+ }
638
+ ```
639
+
640
+ As ilhas Preact funcionam da mesma forma: com `@astrojs/preact` e `compat: true`, `react` resolve para `preact/compat` tanto no build do servidor quanto no do cliente, e o import acima permanece inalterado.
641
+
642
+ Sem ilhas, em uma página sem nenhum framework no cliente, o mesmo estado vive no frontmatter por meio de [`decodeState` e `encodeState`](#helpers-encodestate-e-decodestate):
643
+
644
+ ```astro
645
+ ---
646
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
647
+
648
+ import { form } from '../state';
649
+
650
+ const state = decodeState(Astro.url.searchParams, form);
651
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
652
+ ---
653
+
654
+ <pre>{JSON.stringify(state)}</pre>
655
+ <a href={`?${withName}`}>Alice</a>
656
+ ```
657
+
658
+ [Exemplo de código](packages/example-astro/src/components/Form-for-test.tsx), [página em Astro puro](packages/example-astro/src/pages/pure-astro.astro)
659
+
518
660
  ## Receitas
519
661
  ##### Hook personalizado para trabalhar convenientemente com uma fatia do estado
520
662
  <details>
@@ -787,7 +929,7 @@ Veja o [documento de contribuição](CONTRIBUTING.md)
787
929
  - [x] hook para `react-router`
788
930
  - [x] hook para `remix`
789
931
  - [ ] hook para `svelte`
790
- - [ ] hook para `astro`
932
+ - [x] hook para `astro`
791
933
  - [ ] hook para armazenar estado no hash ?
792
934
 
793
935
  ## Contato e suporte