You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 00:24:51