阅读视图

发现新文章,点击刷新页面。

从 useState 到 URLState:前端状态管理的另一种思路

在 React 开发中,useState 是我们最常用的状态管理工具之一。它轻量、直观,能满足大多数组件级状态管理需求。但在某些场景下,比如列表筛选、分页、多页面共享状态时,单纯使用 useState 会遇到状态丢失、刷新页面重置等问题。这时候,URLState 或许是一个更优雅的解决方案。

一、useState 的「痛点」场景

先来看一个常见的业务场景:实现一个带筛选功能的商品列表页,包含「价格区间」「分类」「排序方式」三个筛选条件。用 useState 实现的代码可能是这样的:


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 可能变成这样:


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 核心逻辑拆解

  1. 从 URL 中解析初始状态:通过 new URLSearchParams(window.location.search) 获取查询参数。

  2. 定义状态更新函数:修改状态时,同步更新 URL(使用 history.pushState 避免页面刷新)。

  3. 监听 URL 变化:当用户通过前进/后退按钮切换历史记录时,同步更新组件状态。

3.2 完整实现代码


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 带来的痛点:


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 会自动更新为:


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、全局状态库是互补关系。在合适的场景选择合适的工具,才是高效开发的关键。

❌