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

useUrlState — trực tiếp với next.js

Gõ bên dưới — xem URL sáng lên

Client component đầu tiên
Client component khác

Đọc từ URL — không props, không context, kiểu và cấu trúc được giữ nguyên

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

Cùng một API, ba router

Bắt đầu nhanh

1. Định nghĩa state
state
export const form: Form = {
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

// use `Type` not `Interface`!
type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: { id: string; value: { text: string; time: Date } }[];
};
2. Dùng trong bất kỳ component nào
ComponentA
'use client';

import { useUrlState } from 'state-in-url/next';
import { form } from './form';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
  // useHistory: false to update sp on server component
  const { urlState, setState, setUrl } = useUrlState(form, { useHistory: true }); 

  return <>
    <input
      id="name"
      value={urlState.name} 
      onChange={(ev) => setUrl({ name: ev.target.value })}
      />
    // OR can update state immediately but sync change to url as needed
    <input
      value={urlState.name}
      onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
      onBlur={() => setUrl()}
    />
    <button onClick={() => setUrl((curr, initial) => initial)}>
      Reset
    </button>
    </>
};
ComponentB
'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';

// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
  const { urlState } = useUrlState(form, { searchParams });

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
3. Tạo hook tái sử dụng cho một phần state
useFormState - custom hook
'use client';

import React from 'react';
import { useUrlState } from 'state-in-url/next';

const form: Form={
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: {id: string; value: {text: string; time: Date } }[];
};

export const useFormState = ({ searchParams }: { searchParams?: object }) => {
  const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
    searchParams,
  });

  // first navigation will push new history entry
  // all following will just replace that entry
  // this way will have history with only 2 entries - ['/url', '/url?key=param']

  const replace = React.useRef(false);
  const setUrl = React.useCallback((
      state: Parameters<typeof setUrlBase>[0],
      opts?: Parameters<typeof setUrlBase>[1]
    ) => {
      setUrlBase(state, { replace: replace.current, ...opts });
      replace.current = true;
  }, [setUrlBase]);

  return { urlState, setUrl, resetUrl: reset };
};

Đang dùng AI coding agent?

Các agent mắc cùng hai lỗi ở đây, mọi lúc. Chúng định nghĩa hình dạng state bằng interface, mà ràng buộc JSONCompatible từ chối ngay lập tức. Và chúng xây dựng object state mặc định bên trong component, điều này ngầm phá vỡ việc chia sẻ — nó được khóa theo định danh object, nên không có lỗi, hai component chỉ đơn giản ngừng nhìn thấy nhau.

Vậy nên package kèm theo sáu SKILL.md file. Agent của bạn tải một file theo yêu cầu qua TanStack Intent, và chúng được quản lý phiên bản cùng thư viện chứ không phải cùng trang này.

npx @tanstack/intent@latest install

Chạy một lần trong dự án đã cài state-in-url . Sau đó agent của bạn tìm các skill trong node_modules/state-in-url/skills/.

  • feature-state-hookĐịnh nghĩa state, và bọc useUrlState trong một hook theo phạm vi tính năng
  • input-handlingÔ nhập văn bản, thanh trượt, bất cứ thứ gì thay đổi nhanh
  • nextjs-ssrApp Router: chuyển tiếp searchParams, Proxy cho layout
  • react-router-remix-setupThiết lập React Router v6/v7 hoặc Remix v2
  • form-library-integrationKết hợp với react-hook-form (hoặc formik)
  • shared-state-no-urluseSharedState — chia sẻ mà không chạm vào URL

Nguồn ở trên GitHub. Agent không thể tải các skill Intent nên đọc llms.txt thay thế — cùng các quy tắc, cô đọng trong một file.

Tại sao là state-in-url?

Các thư viện state trong URL đã tồn tại, nhưng hầu hết hoặc rườm rà khi thiết lập, hoặc hạn chế về thứ có thể lưu. state-in-url hướng tới việc đơn giản là hoạt động: một API phản chiếu React.useState, với URL làm nơi lưu trữ.

Lưu state không cần boilerplate, xây deep link và chia sẻ dữ liệu giữa các client component không liên quan — không cần provider. Cấu trúc và kiểu được giữ nguyên từ đầu đến cuối: một Date vào, một Date ra.

Được xây dựng theo test-first, với bộ test đơn vị và e2e đa trình duyệt chạy trên mỗi commit.

Next.js: không cần ranh giới Suspense

Hook không bao giờ gọi useSearchParams, nên component dùng nó không cần bọc trong Suspense và không khiến trang bị loại khỏi prerendering — bao gồm PPR và cacheComponents . Nó đọc URL trực tiếp và theo dõi mọi thay đổi sau đó, bao gồm cả history.pushState từ code không biết gì về nó.

Không dùng Next.js hoặc react-router?

Các helper encodeState / decodeState hoạt động với bất kỳ framework hoặc JS thuần — các hook là sự tiện lợi bên trên.

Hãy xem trang GitHub — một star giúp ích rất nhiều.

Chia sẻ với các nhà phát triển khác

Uneed Embed Badge