react-router-dom问题:Catalog页面未感知搜索参数更新
在线商店搜索参数更新后Catalog组件不刷新问题
我正在开发一款带商品目录(Catalog)和搜索输入框的在线商店,目前逻辑是:在非Catalog页面按下回车键时,会跳转至Catalog页面,并将搜索词写入URL搜索参数。
核心问题:当处于Catalog页面时,输入新搜索词后URL已更新,但Catalog组件无法感知到搜索参数的变化,导致搜索结果无法更新。
相关代码
Header.tsx
import Logo from '@/components/shared/Logo/Logo'; import Quantity from '@/components/shared/Quantity/Quantity'; import { toast } from '@/components/ui/use-toast'; import { PagePaths } from '@/enum/PagePaths'; import { useActions } from '@/hooks/general/useActions'; import { useTypedSelector } from '@/hooks/general/useTypedSelector'; import { getBasketQuantitySelector } from '@/store/slices/basket/basket.selectors'; import { getFavouriteQuantitySelector } from '@/store/slices/favourites/favourites.selectors'; import { getUserInfoSelector } from '@/store/slices/user/user.selectors'; import { Link, useNavigate } from 'react-router-dom'; import classes from './Header.module.scss'; import SearchInput from "./components/SearchInput"; function Header() { const { isAuth, isAdmin } = useTypedSelector(getUserInfoSelector); const { logout } = useActions(); const favouritesQuantity = useTypedSelector(getFavouriteQuantitySelector); const basketItemsQuantity = useTypedSelector(getBasketQuantitySelector); const navigate = useNavigate(); async function handleButton() { if (!isAuth) return navigate(PagePaths.AUTHENTICATION.LOGIN); logout(); toast({ title: `Success!` }); } return ( <header className={classes.header}> <Logo /> <SearchInput placeholder="Search..." /> <div className={classes.header__right}> <Link to="/favourites" className={[classes.header__btn, classes.header__favourite].join(' ')} > <Quantity quantity={favouritesQuantity} /> </Link> <Link to={PagePaths.CART} className={[classes.header__btn, classes.header__cart].join(' ')} > <Quantity quantity={basketItemsQuantity} /> </Link> <button onClick={handleButton}>{isAuth ? 'Logout' : 'Sign in'}</button> {isAdmin && <Link to={PagePaths.ADMIN.HOME}>Admin</Link>} </div> </header> ); } export default Header;
SearchInput.tsx
import { PagePaths } from '@/enum/PagePaths'; import { useMySearchParams } from '@/hooks/general/useMySearchParams'; import { InputHTMLAttributes, KeyboardEvent } from 'react'; import { useNavigate } from 'react-router-dom'; interface SearchInputProps extends InputHTMLAttributes<HTMLInputElement> {} function SearchInput({ placeholder }: SearchInputProps) { const navigate = useNavigate(); const searchTerm = useMySearchParams('searchTerm'); function handleEnterDown(e: KeyboardEvent<HTMLInputElement>) { if (e.key !== 'Enter') return; navigate(`${PagePaths.CATALOG}?searchTerm=${e.currentTarget.value}`); } return ( <form className="max-w-[400px] w-full" role="search" onSubmit={(e) => e.preventDefault()} > <input className="w-full py-1 px-3 rounded-md" type="search" placeholder={placeholder} defaultValue={searchTerm || ''} aria-label="Search" onKeyDown={handleEnterDown} /> </form> ); } export default SearchInput;
Catalog.tsx
import CardsContainer from '@/components/shared/CardsContainer/CardsContainer'; import { Sheet, SheetHeader, SheetTitle, SheetTrigger, SheetContent as SheetWrapper, } from '@/components/ui/sheet'; import { useProducts } from '@/hooks/features/useProducts/useProducts'; import { Settings2 as FiltersIcon } from 'lucide-react'; import CatalogPagination from './components/CatalogPagination'; import FiltersLayout from './components/FiltersLayout/FiltersLayout'; import { useFilters } from './useFilters'; function Catalog() { const { filters, changeFilters, page, changePage, searchTerm } = useFilters(); const { data } = useProducts({ filters, page, searchTerm }); const isPaginationNeeded = data && data.totalPages > 1; const isProductsFound = data?.products && data?.products.length !== 0; return ( <section className="rows-container"> <Sheet> <div className="flex gap-5 items-center mb-3"> <strong className="subtitle">Catalog</strong> <SheetTrigger className="link flex items-center gap-2 py-1 px-2 rounded-md"> <FiltersIcon /> Show filters </SheetTrigger> </div> <SheetWrapper> <SheetHeader className="mb-7"> <SheetTitle className="flex items-center gap-2"> <FiltersIcon /> Filters </SheetTitle> </SheetHeader> {/* Sheet Content */} <div> <FiltersLayout filters={filters} changeFilters={changeFilters} /> </div> {/* Sheet Content END */} </SheetWrapper> </Sheet> {isProductsFound ? ( <CardsContainer products={data.products} /> ) : ( <p>Empty...</p> )} {isPaginationNeeded && ( <CatalogPagination currentPage={page} totalPages={data.totalPages} setCurrentPage={changePage} /> )} </section> ); } export default Catalog;
useFilters.tsx
import { IProductFitlers } from '@/hooks/features/useProducts/filters.types'; import { ProductQueryData } from '@/services/product/product.types'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { configurateUrlParams, parseParamsFromUrl } from './catalog.helpers'; export const useFilters = () => { const [_, setSearchParams] = useSearchParams(); const [page, setPage] = useState(1); const [filters, setFilters] = useState<IProductFitlers>({}); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { const params = window.location.search.slice(1); if (!params) return; const queryData = parseParamsFromUrl<ProductQueryData>(params, { tryToParseNumbersInArray: true, tryToParseNumbersInObject: true, tryToParsePrimitive: true, }); setFilters(queryData?.filters || {}); setPage(queryData?.page ? +queryData.page : 1); setSearchTerm(queryData.searchTerm || ''); }, []); function changeFilters(newFilters: IProductFitlers) { const isReset = Object.keys(newFilters).length === 0; let objectToServer: ProductQueryData = { page: 1 }; if (!isReset) { objectToServer = { ...objectToServer, filters: newFilters, searchTerm, }; } else setSearchTerm(''); setSearchParams(configurateUrlParams(objectToServer)); setFilters(newFilters); setPage(1); } function changePage(newPage: number) { setSearchParams( configurateUrlParams({ filters, page: newPage, searchTerm }) ); setPage(newPage); } return { filters, changeFilters, page, changePage, searchTerm }; };
解决方案
问题根源在于useFilters中的useEffect仅初始化执行一次,无法监听URL参数变化;同时SearchInput在Catalog页面的跳转逻辑会导致组件无法感知参数更新。
1. 修改useFilters.tsx,监听搜索参数变化
将useEffect的依赖项改为searchParams,确保参数变化时重新解析并更新状态:
export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); const [page, setPage] = useState(1); const [filters, setFilters] = useState<IProductFitlers>({}); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { const params = searchParams.toString(); if (!params) return; const queryData = parseParamsFromUrl<ProductQueryData>(params, { tryToParseNumbersInArray: true, tryToParseNumbersInObject: true, tryToParsePrimitive: true, }); setFilters(queryData?.filters || {}); setPage(queryData?.page ? +queryData.page : 1); setSearchTerm(queryData.searchTerm || ''); }, [searchParams]); // 监听searchParams变化 // 其余代码保持不变 };
2. 优化SearchInput.tsx的参数更新逻辑
在Catalog页面时直接更新搜索参数,避免页面跳转:
import { PagePaths } from '@/enum/PagePaths'; import { useMySearchParams } from '@/hooks/general/useMySearchParams'; import { InputHTMLAttributes, KeyboardEvent } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; interface SearchInputProps extends InputHTMLAttributes<HTMLInputElement> {} function SearchInput({ placeholder }: SearchInputProps) { const navigate = useNavigate(); const searchTerm = useMySearchParams('searchTerm'); const location = useLocation(); const [_, setSearchParams] = useSearchParams(); function handleEnterDown(e: KeyboardEvent<HTMLInputElement>) { if (e.key !== 'Enter') return; const value = e.currentTarget.value.trim(); if (location.pathname === PagePaths.CATALOG) { // 直接更新参数,保留现有筛选和分页信息 const newParams = new URLSearchParams(location.search); if (value) { newParams.set('searchTerm', value); } else { newParams.delete('searchTerm'); } setSearchParams(newParams); } else { // 非Catalog页面跳转 navigate(`${PagePaths.CATALOG}?searchTerm=${value}`); } } return ( <form className="max-w-[400px] w-full" role="search" onSubmit={(e) => e.preventDefault()} > <input className="w-full py-1 px-3 rounded-md" type="search" placeholder={placeholder} defaultValue={searchTerm || ''} aria-label="Search" onKeyDown={handleEnterDown} /> </form> ); } export default SearchInput;
3. 确认useProducts响应参数变化
确保useProducts的useEffect依赖包含filters、page、searchTerm,保证参数变化时重新发起请求:
// 示例useProducts实现(按需调整) export const useProducts = ({ filters, page, searchTerm }) => { const [data, setData] = useState(null); useEffect(() => { const fetchProducts = async () => { // 此处替换为实际接口请求逻辑 const response = await fetch(`/api/products?page=${page}&searchTerm=${searchTerm}&filters=${JSON.stringify(filters)}`); const result = await response.json(); setData(result); }; fetchProducts(); }, [filters, page, searchTerm]); // 依赖参数变化 return { data }; };
内容的提问来源于stack exchange,提问作者dyakonovr
相关产品推荐
相关产品推荐

