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

state-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-urlnuqs
Cài đặtKhông cần — import hook là dùng đượcComponent adapter bọc quanh ứng dụng
Hình dạng stateMột object có kiểu, giống React.useStateGiá 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 componentBọc hook một lần — mọi component chia sẻ state, không cần propsBạn tự tách hook quanh bộ parser
Object và mảng lồng nhauCó sẵn — giữ nguyên cấu trúc và kiểuParser JSON cộng thêm validator tự viết
Ngày thángGiữ nguyên tự độngParser 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 runtimeKhôngMột
RouterNext.js, React Router v6/v7, Remix, helper cho JS thuầnNext.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 (nuqs)
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
filters.tsx (nuqs)
'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 })}
    />
  );
};
filters.tsx (state-in-url)
'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ệnCài đặtObject lồng nhau và ngàyKích thướcChọn khi
state-in-urlKhông cần — import hookGiữ tự động, kèm kiểu~2.9 KB gzip, không phụ thuộcMuốn một object có kiểu, không cấu hình, trên Next.js, React Router hoặc Remix
nuqsComponent adapter, parser theo khóaParser JSON cộng validator tự viết~6.7 KB gzip, một phụ thuộcMuốn mỗi giá trị là một query param dễ đọc
TanStack RoutervalidateSearch trên từng routeJSON-first cho object và mảng; ngày cần tự serializeTích hợp trong routerĐang dùng TanStack Router — dùng thứ có sẵn
use-query-paramsProvider cộng adapter router, cấu hình từng paramQua kiểu param JSON, kiểu lỏng~4.4 KB gzip cộng serialize-query-paramsCodebase đã xây trên nó
useSearchParamsKhông cần — router tích hợp sẵnChỉ chuỗi — parse, kiểu và mặc định tự lo0 KBChỉ 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.