在React和Next.js项目中,多条件数据筛选功能需要同时兼顾用户交互体验和状态持久化需求,核心是要让筛选条件的变化能同步到URL参数,同时页面加载时能正确解析URL参数还原筛选状态。这种实现方式既能让用户通过分享链接传递筛选结果,也能避免页面刷新后筛选条件丢失的问题。

核心实现思路
整体实现可以分为三个核心部分:筛选条件的状态管理、URL参数的读写同步、筛选逻辑与数据请求的联动。首先需要定义统一的筛选条件数据结构,然后监听筛选条件变化同步更新URL参数,同时监听URL参数变化更新筛选状态,最后根据当前筛选条件发起数据请求。
筛选条件状态定义
我们可以先定义一个基础的筛选条件类型,包含常见的筛选维度,比如关键字、分类、排序方式、页码等。以下是TypeScript类型定义示例:
// 定义筛选条件类型
type FilterParams = {
keyword?: string; // 搜索关键字
category?: string; // 分类ID
sort?: string; // 排序方式
page?: number; // 当前页码
pageSize?: number; // 每页条数
};
URL参数与状态的同步
Next.js中可以使用useRouter和useSearchParams来操作URL参数,我们需要封装两个工具函数,分别用于把筛选状态转为URL参数字符串,以及从URL参数字符串解析出筛选状态。
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
// 把筛选参数转为URL查询字符串
const filterToQueryString = (filters: FilterParams): string => {
const params = new URLSearchParams();
// 只添加有值的参数,避免冗余
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, String(value));
}
});
return params.toString();
};
// 从URL查询字符串解析筛选参数
const queryStringToFilter = (queryString: string): FilterParams => {
const params = new URLSearchParams(queryString);
return {
keyword: params.get('keyword') || undefined,
category: params.get('category') || undefined,
sort: params.get('sort') || undefined,
page: params.get('page') ? Number(params.get('page')) : undefined,
pageSize: params.get('pageSize') ? Number(params.get('pageSize')) : undefined,
};
};
筛选状态与URL联动的Hook封装
为了方便复用,我们可以封装一个自定义Hook,统一管理筛选状态和URL参数的同步逻辑:
const useFilterWithUrl = (initialFilters: FilterParams = {}) => {
const router = useRouter();
const searchParams = useSearchParams();
// 初始化筛选状态,优先从URL参数读取,没有则使用初始值
const [filters, setFilters] = useState<FilterParams>(() => {
const urlFilters = queryStringToFilter(searchParams.toString());
return { ...initialFilters, ...urlFilters };
});
// 监听筛选状态变化,同步更新URL参数
useEffect(() => {
const queryString = filterToQueryString(filters);
const currentQueryString = searchParams.toString();
// 只有参数变化时才更新路由,避免不必要的跳转
if (queryString !== currentQueryString) {
router.push(`?${queryString}`, { scroll: false });
}
}, [filters, router, searchParams]);
// 监听URL参数变化,同步更新筛选状态
useEffect(() => {
const urlFilters = queryStringToFilter(searchParams.toString());
// 对比当前状态和URL参数,避免循环更新
const isSame = Object.keys(urlFilters).every(key => {
return String(urlFilters[key as keyof FilterParams]) === String(filters[key as keyof FilterParams]);
});
if (!isSame) {
setFilters(prev => ({ ...prev, ...urlFilters }));
}
}, [searchParams]);
// 更新筛选条件的方法
const updateFilter = (newFilters: Partial<FilterParams>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
};
// 重置筛选条件的方法
const resetFilter = () => {
setFilters(initialFilters);
};
return { filters, updateFilter, resetFilter };
};
筛选组件与数据请求联动
接下来我们可以实现一个筛选组件,使用上面封装的Hook管理状态,同时根据筛选条件发起数据请求。以下是筛选组件和数据请求的示例:
import { useState, useEffect } from 'react';
// 模拟数据请求方法
const fetchData = async (filters: FilterParams) => {
// 实际项目中替换为真实的接口请求
console.log('请求数据,筛选条件:', filters);
return { list: [], total: 0 };
};
const FilterPage = () => {
const { filters, updateFilter, resetFilter } = useFilterWithUrl({ page: 1, pageSize: 10 });
const [data, setData] = useState({ list: [], total: 0 });
const [loading, setLoading] = useState(false);
// 监听筛选条件变化,发起数据请求
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const res = await fetchData(filters);
setData(res);
} finally {
setLoading(false);
}
};
loadData();
}, [filters]);
return (
<div>
<div className="filter-bar">
<input
type="text"
placeholder="输入关键字搜索"
value={filters.keyword || ''}
onChange={(e) => updateFilter({ keyword: e.target.value, page: 1 })}
/>
<select
value={filters.category || ''}
onChange={(e) => updateFilter({ category: e.target.value, page: 1 })}
>
<option value="">全部分类</option>
<option value="1">分类一</option>
<option value="2">分类二</option>
</select>
<select
value={filters.sort || ''}
onChange={(e) => updateFilter({ sort: e.target.value })}
>
<option value="">默认排序</option>
<option value="price_asc">价格升序</option>
<option value="price_desc">价格降序</option>
</select>
<button onClick={resetFilter}>重置筛选</button>
</div>
{loading ? (
<p>加载中...</p>
) : (
<div className="data-list">
{/* 渲染数据列表 */}
<p>当前筛选条件:{JSON.stringify(filters)}</p>
</div>
)}
</div>
);
};
export default FilterPage;
注意事项
- 参数编码问题:如果筛选条件包含中文或特殊字符,URLSearchParams会自动进行编码,解析时也会自动解码,不需要额外处理。
- 避免冗余参数:转换URL参数时只添加有值的参数,避免URL中出现无意义的空参数。
- 路由模式适配:如果是Next.js的app路由,使用
useSearchParams即可;如果是pages路由,使用useRouter的query属性即可,逻辑基本一致。 - 性能优化:如果筛选条件变化频繁,可以给数据请求增加防抖处理,避免短时间内发起过多请求。