在 React 开发中,useState 是我们最常用的状态管理工具之一。它轻量、直观,能满足大多数组件级状态管理需求。但在某些场景下,比如列表筛选、分页、多页面共享状态时,单纯使用 useState 会遇到状态丢失、刷新页面重置等问题。这时候,URLState 或许是一个更优雅的解决方案。
一、useState 的「痛点」场景
先来看一个常见的业务场景:实现一个带筛选功能的商品列表页,包含「价格区间」「分类」「排序方式」三个筛选条件。用 useState 实现的代码可能是这样的:
jsx
import { useState } from 'react';
function ProductList() {
// 筛选状态
const [priceRange, setPriceRange] = useState([0, 1000]);
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('price-asc');
// 筛选逻辑...
return (
<div>
<FilterPanel
priceRange={priceRange}
onPriceRangeChange={setPriceRange}
category={category}
onCategoryChange={setCategory}
sortBy={sortBy}
onSortByChange={setSortBy}
/>
<ProductGrid />
</div>
);
}
这段代码看似没问题,但存在三个明显痛点:
-
页面刷新状态丢失:用户筛选后刷新页面,所有筛选条件会重置为初始值,体验极差。
-
状态无法共享:如果需要在其他页面(比如商品详情页)回退到筛选后的列表,无法携带筛选状态。
-
无法书签/分享:用户想把筛选后的结果分享给同事,复制链接过去是未筛选的初始状态。
二、什么是 URLState?为什么要用它?
URLState 是将应用状态存储在 URL 查询参数(Query String)中的状态管理方式。比如上面的筛选场景,使用 URLState 后,URL 可能变成这样:
Plain
https://example.com/products?price=0-1000&category=electronics&sort=price-asc
这种方式的核心优势在于:
-
「刷新不丢状态」:URL 是浏览器的持久化载体,刷新页面参数不会消失。
-
「天然可共享」:复制 URL 即可分享当前状态,支持书签保存。
-
「跨页传参简单」:不同页面间通过 URL 即可传递状态,无需依赖全局状态库。
-
「可回溯」:浏览器的前进/后退按钮能直接回溯状态变更历史。
三、实现 URLState:自定义 useURLState Hook
其实 URLState 的实现并不复杂,核心是通过 URLSearchParams 操作查询参数,并结合 React 的状态更新机制。下面我们封装一个通用的 useURLState Hook。
3.1 核心逻辑拆解
-
从 URL 中解析初始状态:通过
new URLSearchParams(window.location.search)获取查询参数。 -
定义状态更新函数:修改状态时,同步更新 URL(使用
history.pushState避免页面刷新)。 -
监听 URL 变化:当用户通过前进/后退按钮切换历史记录时,同步更新组件状态。
3.2 完整实现代码
jsx
import { useState, useEffect, useCallback } from 'react';
function useURLState(initialState = {}) {
// 从 URL 解析状态
const parseURLState = useCallback(() => {
const searchParams = new URLSearchParams(window.location.search);
const state = {};
// 遍历初始状态,从 URL 中提取对应参数
Object.entries(initialState).forEach(([key, defaultValue]) => {
const value = searchParams.get(key);
if (value === null) {
state[key] = defaultValue;
return;
}
// 处理不同类型的默认值(数字、布尔、数组等)
if (typeof defaultValue === 'number') {
state[key] = Number(value);
} else if (typeof defaultValue === 'boolean') {
state[key] = value === 'true';
} else if (Array.isArray(defaultValue)) {
state[key] = value.split('-');
} else {
state[key] = value;
}
});
return state;
}, [initialState]);
// 初始化状态:从 URL 解析或使用初始值
const [state, setState] = useState(parseURLState());
// 当 URL 变化时(前进/后退),同步更新状态
useEffect(() => {
const handlePopState = () => {
setState(parseURLState());
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [parseURLState]);
// 更新状态并同步到 URL
const setURLState = useCallback((newState) => {
const searchParams = new URLSearchParams(window.location.search);
const nextState = { ...state, ...newState };
// 遍历新状态,更新到 searchParams
Object.entries(nextState).forEach(([key, value]) => {
if (value === initialState[key]) {
// 如果值等于初始值,移除该参数(保持 URL 简洁)
searchParams.delete(key);
} else {
// 数组类型用 "-" 拼接
const paramValue = Array.isArray(value) ? value.join('-') : String(value);
searchParams.set(key, paramValue);
}
});
// 更新 URL(pushState 不会刷新页面)
const searchString = searchParams.toString();
const newUrl = searchString ? `${window.location.pathname}?${searchString}` : window.location.pathname;
window.history.pushState({}, '', newUrl);
// 更新组件状态
setState(nextState);
}, [state, initialState]);
return [state, setURLState];
}
四、实战:用 URLState 重构商品列表页
有了 useURLState,我们可以轻松重构之前的商品列表页,解决 useState 带来的痛点:
jsx
import { useURLState } from './useURLState';
function ProductList() {
// 用 useURLState 替代 useState,初始状态和之前一致
const [state, setURLState] = useURLState({
priceRange: [0, 1000], // 数组类型
category: 'all', // 字符串类型
sortBy: 'price-asc' // 字符串类型
});
const { priceRange, category, sortBy } = state;
// 筛选条件变更时,调用 setURLState 更新
const handlePriceChange = (newRange) => {
setURLState({ priceRange: newRange });
};
const handleCategoryChange = (newCategory) => {
setURLState({ category: newCategory });
};
const handleSortChange = (newSort) => {
setURLState({ sortBy: newSort });
};
return (
<div>
<FilterPanel
priceRange={priceRange}
onPriceRangeChange={handlePriceChange}
category={category}
onCategoryChange={handleCategoryChange}
sortBy={sortBy}
onSortByChange={handleSortChange}
/>
<ProductGrid />
</div>
);
}
此时,用户筛选商品后,URL 会自动更新为:
Plain
https://example.com/products?priceRange=0-2000&category=electronics&sortBy=price-desc
刷新页面、复制链接分享、后退到上一个筛选状态,都能完美生效!
五、URLState 的适用场景与注意事项
5.1 适用场景
-
列表筛选、分页、排序等「可分享」的状态。
-
多步骤表单(如注册流程)的进度状态。
-
单页应用中的「页面级」状态(如标签页切换)。
5.2 注意事项
-
不要存储敏感信息:URL 参数会暴露在地址栏、浏览器历史、服务器日志中,密码、token 等敏感信息绝对不能用 URLState。
-
参数不宜过多/过长:浏览器对 URL 长度有上限(通常 2KB-8KB),复杂状态建议用全局状态库(如 Redux)。
-
处理特殊类型数据:对于对象等复杂类型,需要先序列化(如 JSON.stringify),但会增加 URL 长度,需谨慎使用。
六、总结
useState 是 React 状态管理的基石,但在「状态持久化」「可分享」场景下存在局限。URLState 作为一种轻量级的补充方案,通过 URL 查询参数实现状态的持久化和共享,无需引入复杂的状态管理库,就能解决很多实际业务问题。
当然,URLState 不是银弹,它和 useState、全局状态库是互补关系。在合适的场景选择合适的工具,才是高效开发的关键。