从 useState 到 URLState:为什么大佬们都在删状态管理代码?

1. 前言

当你打开这个网址时:

plain 复制代码
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers

你会发现,所有你需要的主题、语言、插件已经被自动勾选:

当你在页面修改配置时,URL 也会随之改变。

你看,这个 URL 不仅仅是一个链接,更是一个完整的状态容器,保存了我的所有配置。无需数据库、cookie 或 localStorage,一个 URL 就解决了一切。

2. 被忽视的 URL 超能力

URL 是互联网最伟大的创意之一,通过 URL 请求,我们可以查找到网络上的唯一资源。

它的标准格式为:<scheme>://<netloc>/<path>?<query>#<fragment>

但 URL 的价值远不止于此------它们是天然的状态管理解决方案。想想 URL 给我们带来的好处:

  • 可分享性:发送链接,对方会看到与你完全相同的内容
  • 可书签化:保存 URL 就是保存一个特定时刻的状态
  • 浏览器历史:后退按钮正常工作
  • 深度链接:直接跳转到应用的特定状态

URL 使 Web 应用具有韧性和可预测性。它们是 Web 最初的状态管理方案,自 1990 年以来就开始使用,所以千万不要忘记使用这种方式。

3. URL 如何编码状态?

URL 的不同部分编码不同类型的状态:

路径段(/path/to/myfile.html):最适合层次化资源导航

plain 复制代码
/users/123/posts        # 用户123的文章
/docs/api/authentication # 文档结构

查询参数(?key1=value1&key2=value2):完美用于过滤器、选项和配置

plain 复制代码
?theme=dark&lang=en     # UI 偏好设置
?page=2&limit=20        # 分页
?status=active&sort=date # 数据过滤

锚点片段(#SomewhereInTheDocument):适合客户端导航和页面部分

plain 复制代码
#L20-L35        # GitHub 行高亮
#features       # 滚动到某个章节

4. URL 编码状态常见模式

4.1. 多个带分隔符的值

plain 复制代码
?languages=javascript+typescript+python
?tags=frontend,react,hooks

这种方式简洁易读,但需要在服务器端手动解析。

4.2. 嵌套或结构化数据

plain 复制代码
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9==  (base64-encoded JSON)

开发者有时会将复杂的筛选器或配置对象编码到单个查询字符串中。

一种简单的约定是使用逗号分隔的键值对,而其他方法则会序列化 JSON,甚至为了安全起见对其进行 Base64 编码。

4.3. 数组处理(方括号表示法)

plain 复制代码
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73

一种古老的模式是方括号表示法,它用于在查询参数中表示数组。这种表示法起源于早期的 Web 框架,例如 PHP,在 [] 参数名称后添加括号表示多个值应该组合在一起。

许多现代框架和解析器(例如 Node 的 qs 库或 Express 中间件)仍然能够自动识别这种模式。然而,它并未在 URL 规范中正式标准化,因此其行为可能因服务器或客户端的实现而异。

4.4. 布尔处理

对于 flag 或开关,通常会显式传递布尔值,或者依赖于键值是否为真。这样可以缩短 URL 长度,并简化功能切换。

plain 复制代码
?debug=true&analytics=false
?mobile  (presence = true)

4.5. 结论

使用哪种模式都是可以的,关键在于保持一致性。选择适合你应用场景的模式,并坚持使用。

5. 实际应用案例

GitHub 行高亮:

plain 复制代码
https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136

链接到特定文件,同时高亮显示 108-136 行。点击此链接,你会直接定位到讨论的确切代码部分。

电商数据过滤器:

plain 复制代码
https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc

这是最常见的实现。每个过滤条件、排序选项都被保存。用户可以用书签保存他们的筛选条件。

谷歌地图:

plain 复制代码
https://www.google.com/maps/@22.443842,-74.220744,19z

坐标、缩放级别和地图类型都包含在 URL 中。分享此链接,任何人都可以看到完全相同的地图视图。

6. 什么状态应该放入 URL?

然而并非所有状态都应该属于 URL,那什么样的状态应该放入 URL 呢?

适合 URL 状态:

  • 搜索查询和筛选器
  • 分页和排序
  • 视图模式(列表/网格、深色/浅色)
  • 日期范围和时间段
  • 选中项或活动标签
  • 影响内容的 UI 配置
  • 功能开关和 A/B 测试版本

不适合 URL 状态:

  • 敏感信息(密码、令牌、个人身份信息)
  • 临时 UI 状态(模态框打开/关闭)
  • 表单输入进行中(未保存的更改)
  • 极其庞大或复杂的嵌套数据
  • 高频瞬态(鼠标位置、滚轮位置)

简单来说,你的判断标准是:

如果别人点击这个 URL,他们应该看到相同的状态吗?

如果是,它就属于 URL。

7. 实现方案

7.1. 使用纯 JavaScript 实现

现代 URLSearchParams API 使 URL 状态管理变得简单:

javascript 复制代码
// 读取URL参数
const params = new URLSearchParams(window.location.search);
const view = params.get("view") || "grid"; // 默认值
const page = parseInt(params.get("page")) || 1;

// 更新URL参数
function updateFilters(filters) {
  const params = new URLSearchParams(window.location.search);

  params.set("status", filters.status);
  params.set("sort", filters.sort);

  // 更新URL而不重新加载页面
  const newUrl = `${window.location.pathname}?${params.toString()}`;
  window.history.pushState({}, "", newUrl);
}

// 处理后/前进按钮
window.addEventListener("popstate", () => {
  const params = new URLSearchParams(window.location.search);
  const filters = {
    status: params.get("status") || "all",
    sort: params.get("sort") || "date",
  };
  renderContent(filters);
});

7.2. 使用 React 实现

React Router 提供了更简洁的钩子:

jsx 复制代码
import { useSearchParams } from "react-router-dom";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const color = searchParams.get("color") || "all";
  const sort = searchParams.get("sort") || "price";

  const handleColorChange = (newColor) => {
    setSearchParams((prev) => {
      const params = new URLSearchParams(prev);
      params.set("color", newColor);
      return params;
    });
  };

  return (
    <select value={color} onChange={(e) => handleColorChange(e.target.value)}>
      <option value="all">所有颜色</option>
      <option value="silver">银色</option>
    </select>
  );
}

8. URL 使用最佳实践

8.1. 优雅处理默认值

不要在 URL 中使用默认值:

jsx 复制代码
// ❌
?theme=light&lang=en&page=1&sort=date

// ✅
?theme=dark  // light 是默认的,但 dark 不是默认的

在代码中读取参数时使用默认值:

jsx 复制代码
function getTheme(params) {
  return params.get("theme") || "light"; // 在代码中设置默认值
}

8.2. URL 更新防抖动

对于高频更新(例如边输入边搜索),要对 URL 更改进行防抖处理:

jsx 复制代码
import { debounce } from "lodash";

const updateSearchParam = debounce((value) => {
  const params = new URLSearchParams(window.location.search);
  if (value) {
    params.set("q", value);
  } else {
    params.delete("q");
  }
  window.history.replaceState({}, "", `?${params.toString()}`);
}, 300);

8.3. URL 传达意义

jsx 复制代码
https://example.com/p?id=x7f2k&v=3 ❌
https://example.com/products/laptop?color=silver&sort=price ✅

第一个链接隐藏了意图,第二个链接则意义清晰。人可以阅读它并理解其含义。机器可以解析它并提取有意义的结构。这才是优秀的 URL。

9. 使用时要避免的反模式

9.1. 状态都保存在内存中的单页应用程序

plain 复制代码
// 用户一刷新,状态都丢失了
const [filters, setFilters] = useState({});

如果你的应用在刷新后丢失了之前的状态,你就破坏了网络的一项基本功能。用户期望 URL 能够保留上下文。

9.2. 包含敏感数据

plain 复制代码
// 别这样干
?password=secret123

9.3. 命名不一致或晦涩难懂

plain 复制代码
// 晦涩难懂
?foo=true&bar=2&x=dark

// 自文档化且风格保持一致
?mobile=true&page=2&theme=dark

9.4. 注意 URL 长度限制

浏览器和服务器对 URL 长度都有实际的限制(通常在 2000 到 8000 个字符之间),但实际情况更为复杂,会有来自浏览器行为、服务器配置、CDN 甚至搜索引擎的限制等多种因素。

如果你遇到了这些限制,那就说明你需要重新考虑你的策略了。

10. 总结

好的 URL 不仅仅是指向内容,它更是描述了用户和应用程序之间的对话。

我们已经构建了复杂的状态管理库,但有时最好的解决方案其实是最简单的那一个。当你的应用在点击刷新时失去了状态,想一想,你是否错过了这个 Web 最古老、最优雅的特性?

11. 参考链接

  1. Your URL Is Your State
相关推荐
烟袅1 小时前
一文看懂 Promise:异步任务的“执行流程控制器”
前端·javascript
zhuweileo1 小时前
npx命令的作用
前端
AiXed1 小时前
PC微信 device uuid 算法
前端·算法·微信
郑州光合科技余经理1 小时前
乡镇外卖跑腿小程序开发实战:基于PHP的乡镇同城O2O
java·开发语言·javascript·spring cloud·uni-app·php·objective-c
桃桃乌龙_95271 小时前
IntersectionObserver实现横向虚拟滚动列表
前端·vue.js
float_六七2 小时前
SQL中的NULL陷阱:为何=永远查不到空值
java·前端·sql
小满zs2 小时前
Next.js第三章(App Router)
前端
Sheldon一蓑烟雨任平生2 小时前
Vue3 KeepAlive(缓存组件实例)
vue.js·vue3·组件缓存·keepalive·缓存组件实例·onactivated·ondeactivated
小满zs2 小时前
Next.js第二章(项目搭建)
前端