State có kiểu, sống trong URL
useUrlState là state React tự ghi chính nó vào chuỗi truy vấn. Object, array và ngày giữ nguyên kiểu, mọi state là một liên kết có thể chia sẻ và tồn tại qua các lần tải lại — không provider, không boilerplate.
- ~2 KB nén gzip
- không phụ thuộc
- TypeScript-first
- Next.js / react-router / Remix
- MIT
npm i state-in-urlstate-in-url vs nuqs
Đang tìm một lựa chọn thay thế nuqs? Cả hai đều lưu state có kiểu trong query string; khác nhau ở lượng cấu hình cần thiết và giá trị có thể là gì.
| Tiêu chí | state-in-url | nuqs |
|---|---|---|
| Cài đặt | Không cần — import hook là dùng được | Component adapter bọc quanh ứng dụng |
| Hình dạng state | Một object có kiểu, giống React.useState | Giá trị theo từng khóa, mỗi khóa khai báo một parser |
| Tái sử dụng giữa các component | Bọc hook một lần — mọi component chia sẻ state, không cần props | Bạn tự tách hook quanh bộ parser |
| Object và mảng lồng nhau | Có sẵn — giữ nguyên cấu trúc và kiểu | Parser JSON cộng thêm validator tự viết |
| Ngày tháng | Giữ nguyên tự động | Parser có sẵn, khai báo theo từng khóa |
| Kích thước, import toàn bộ | ~2.9 KB gzip | ~6.7 KB gzip |
| Phụ thuộc runtime | Không | Một |
| Router | Next.js, React Router v6/v7, Remix, helper cho JS thuần | Next.js, React Router, Remix, TanStack Router, React thuần |
Kích thước: import cả thư viện, esbuild minify + gzip, đo tháng 8/2026 với nuqs 2.10.1.
nuqs là một thư viện tốt — hãy chọn nó khi bạn muốn mỗi giá trị là một query param dễ đọc, hoặc đang dùng TanStack Router. Chọn state-in-url khi bạn muốn cả một object có kiểu nằm trong URL mà không cần cấu hình.
Cùng một tính năng, trong cả hai
Một panel bộ lọc: chuỗi tìm kiếm, số trang, danh sách tag và một ngày. nuqs khai báo parser cho từng khóa và nối adapter ở gốc; state-in-url nhận object và bọc nó trong một hook tái sử dụng.
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'use client';
import {
useQueryStates,
parseAsString,
parseAsInteger,
parseAsArrayOf,
parseAsIsoDateTime,
} from 'nuqs';
export const Filters = () => {
const [filters, setFilters] = useQueryStates({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
tags: parseAsArrayOf(parseAsString).withDefault([]),
since: parseAsIsoDateTime,
});
return (
<input
value={filters.q}
onChange={(ev) => setFilters({ q: ev.target.value, page: 1 })}
/>
);
};'use client';
import { useUrlState } from 'state-in-url/next';
export const filters = {
q: '',
page: 1,
tags: [] as string[],
since: undefined as Date | undefined,
};
// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);
export const SearchBox = () => {
const { urlState, setUrl } = useFilters();
return (
<input
value={urlState.q}
onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
/>
);
};
export const ActiveTags = () => {
// Same state, another component - no props, no context
const { urlState } = useFilters();
return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};Một custom hook đó là toàn bộ API của tính năng: mọi component gọi nó đều chia sẻ cùng một state có kiểu — danh sách tag vẫn là mảng, ngày trả về đúng là object Date. Không props, không context, không nối dây theo từng khóa.
Cài đặt và boilerplate
nuqs nối vào router qua một component adapter bọc quanh ứng dụng, và mỗi mảnh state khai báo parser riêng. state-in-url có sẵn hook cho từng router — import đúng cái, đưa vào object state mặc định, xong. Không gì phải bọc gì.
Next.js, SSR và prerender
Trên App Router, state-in-url không bao giờ gọi useSearchParams, nên component dùng nó không cần ranh giới Suspense và trang vẫn prerender được — kể cả PPR. Server component đọc cùng state qua prop searchParams, được chuyển tiếp nguyên vẹn.
Di chuyển từ nuqs
Đa phần chỉ là thao tác máy móc: gom các khóa của một tính năng vào một object state mặc định, bỏ các khai báo parser — giá trị có kiểu mang cùng thông tin — và thay các setter theo khóa bằng một setter nhận partial. Mỗi trường cấp cao nhất vẫn ứng với query param riêng của nó.
Các lựa chọn khác so ra sao
nuqs không phải lựa chọn thay thế duy nhất. Cùng một việc — state có kiểu trong query string — cũng được router tích hợp sẵn và các thư viện cũ hơn đảm nhận, mỗi cái một đánh đổi.
| Thư viện | Cài đặt | Object lồng nhau và ngày | Kích thước | Chọn khi |
|---|---|---|---|---|
| state-in-url | Không cần — import hook | Giữ tự động, kèm kiểu | ~2.9 KB gzip, không phụ thuộc | Muốn một object có kiểu, không cấu hình, trên Next.js, React Router hoặc Remix |
| nuqs | Component adapter, parser theo khóa | Parser JSON cộng validator tự viết | ~6.7 KB gzip, một phụ thuộc | Muốn mỗi giá trị là một query param dễ đọc |
| TanStack Router | validateSearch trên từng route | JSON-first cho object và mảng; ngày cần tự serialize | Tích hợp trong router | Đang dùng TanStack Router — dùng thứ có sẵn |
| use-query-params | Provider cộng adapter router, cấu hình từng param | Qua kiểu param JSON, kiểu lỏng | ~4.4 KB gzip cộng serialize-query-params | Codebase đã xây trên nó |
| useSearchParams | Không cần — router tích hợp sẵn | Chỉ chuỗi — parse, kiểu và mặc định tự lo | 0 KB | Chỉ một hai param chuỗi phẳng, không đáng thêm thư viện |
Câu hỏi thường gặp
- state-in-url có phải lựa chọn thay thế nuqs tốt không?
- Có, khi bạn muốn cả một object có kiểu nằm trong URL mà không cần cấu hình: không component adapter, không parser theo khóa, object lồng nhau và ngày tháng được giữ tự động. nuqs vẫn là lựa chọn tốt hơn khi bạn muốn mỗi giá trị là một query param dễ đọc, hoặc đang dùng TanStack Router.
- state-in-url hay nuqs nhỏ hơn?
- Đo bằng esbuild (minify + gzip, import cả thư viện) tháng 8/2026: state-in-url khoảng 2.9 KB, không phụ thuộc runtime; nuqs 2.10.1 khoảng 6.7 KB với một phụ thuộc. Import một phần thì cả hai đều nhỏ lại.
- state-in-url có cần adapter hay provider không?
- Không. Mỗi router có entry point riêng — import đúng hook, đưa vào object state mặc định là chạy. Không có component adapter bọc ứng dụng, không có context provider phải cấu hình.
- Di chuyển từ nuqs sang state-in-url có khó không?
- Thường là không: gom các khóa của một tính năng vào một object state mặc định, bỏ các khai báo parser, và thay setter theo khóa bằng một setter nhận partial. Mỗi trường cấp cao nhất vẫn ứng với query param riêng.
- Còn search params của TanStack Router?
- Nếu bạn dùng TanStack Router, hãy dùng thứ có sẵn: search params JSON-first, kiểm tra bằng validateSearch trên từng route. state-in-url và nuqs có ý nghĩa khi router của bạn là Next.js, React Router hoặc Remix — nơi không có search params có kiểu tích hợp.
