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

我是否滥用useEffect?基于更新查询的数据过滤方案咨询

植物商城过滤功能优化与SEO疑问解答

问题背景

我实现了植物商城的过滤功能:用户点击复选框时过滤商品数据、更新URL查询参数,刷新页面后过滤状态能保留。现有两个核心组件:

  • ShopPlants:负责展示商品,并基于URL查询参数过滤数据
  • ShopNavPlants:管理过滤状态,同步更新URL查询参数

当前功能正常,但代码显得繁琐,想确认是否存在过度设计或不良实践;另外数据通过getStaticProps从MongoDB预获取,担心前端useEffect过滤会影响SEO效果,恳请给出建议。

现有实现方案

  • 基于Next.js的useRouter监听路由变化
  • ShopPlants组件以router.isReady和router.query为依赖,路由就绪时执行过滤逻辑
  • ShopNavPlants组件使用两个useEffect:
    • 结合防抖处理过滤状态变化,同步更新URL查询参数
    • 在路由就绪时从URL查询参数恢复过滤状态(解决首次渲染时router.query为undefined的问题)

现有代码展示

ShopPlants 组件

const ShopPlants = (props) => {
  // State handles current display of products
  const [products, setProducts] = useState(
    sortProducts(props.products, "featured")
  );
  const router = useRouter();

  // Handles sorting and updates display
  const sortHandler = (curSort) => {
    const sortedProducts = sortProducts(products, curSort);
    setProducts(sortedProducts);
  };

  // Handles filtering data based on query changes
  useEffect(() => {
    // FILTER HERE
    if (router.isReady) {
      // checks if query 'features' for filtering exists to filter accordingly
      if (router.query?.features) {
        // convert query to an array and return filteredItems
        const toArray = router.query.features.split("&");
        const filteredItems = props.products.filter((product) => {
          const bool = toArray.map((fitlerName) => product[fitlerName]);
          return bool.every((item) => item);
        });

        // set current products display to filtered data
        setProducts(sortProducts(filteredItems, "featured"));
        return;
      }

      // if no filter query is found, return default
      setProducts(sortProducts(props.products, "featured"));
    }
  }, [router.isReady, router.query]);

  return (
    <section className="shop">
      <div className="shop__container section-container">
        <div className="shop__heading">
          <h1 className="heading-secondary">Shop Plants</h1>
          <ShopSort sortedHandler={sortHandler} />
        </div>
        <div className="shop__main">
          <aside className="shop__sidebar">
            <ShopNavPlants />
          </aside>

          <div className="shop__products">
            {products.map((product) => {
              return <ProductOverview key={product.id} product={product} />;
            })}
          </div>
        </div>
      </div>
    </section>
  );
};

ShopNavPlants 组件

// TODO: REFACTOR FILTERING, ADD FILTERING HOOK?
const filterReducer = (state, action) => {
  switch (action.type) {
    case "ADD_FILTER": {
      const updatedState = [...state];
      updatedState.push(action.filter);

      return updatedState;
    }

    case "REMOVE_FILTER": {
      const updatedState = [...state].filter((name) => {
        return name !== action.filter;
      });

      return updatedState;
    }
    case "ACTIVE_FILTERS": {
      const updatedState = [...action.filterList];
      return updatedState;
    }

    default:
      return state;
  }
};

const ShopNavPlants = () => {
  const router = useRouter();
  const [filterStates, dispatch] = useReducer(filterReducer, []);

  // Handles filterState change and pushes filters to query
  useEffect(() => {
    //  prevent error with debounce timer (too many url changes)
    const debounceTimer = setTimeout(() => {
      if (!filterStates || filterStates.length === 0) {
        router.push({});
        return;
      }
      const filterQuery = filterStates.join("&");
      router.query.features = filterQuery;
      router.push({
        query: {
          ...router.query,
        },
      });
    }, 500);

    return () => clearTimeout(debounceTimer);
  }, [filterStates]); 
  
  // Handles any active filters on mount (refresh)
  useEffect(() => {
    if (router.isReady) {
      if (router.query?.features) {
        dispatch({
          type: "ACTIVE_FILTERS",
          filterList: router.query.features.split("&"),
        });
      }
    }
  }, [router.isReady]);

  const checkboxHandler = (e) => {
    !e.target.checked
      ? dispatch({ type: "REMOVE_FILTER", filter: e.target.name })
      : dispatch({ type: "ADD_FILTER", filter: e.target.name });
  };

  return (
    <nav>
      <ul className="shop__sidebar__list-filter">
        <li className="shop__sidebar__item">
          <input
            type="checkbox"
            onChange={checkboxHandler}
            name="petFriendly"
            checked={filterStates.includes("petFriendly") ? true : false}
          />
          Pet-Friendly
        </li>
        <li className="shop__sidebar__item">
          <input
            type="checkbox"
            onChange={checkboxHandler}
            name="lowLight"
            checked={filterStates.includes("lowLight") ? true : false}
          />
          Low-Light
        </li>
        <li className="shop__sidebar__item">
          <input
            type="checkbox"
            onChange={checkboxHandler}
            name="lowMaintenance"
            checked={filterStates.includes("lowMaintenance") ? true : false}
          />
          Low-Maintenance
        </li>
      </ul>
    </nav>
  );
};

疑问解答与优化建议

一、代码设计优化建议

当前实现功能正常,但可以通过以下方式简化代码、消除潜在问题:

  1. 提取自定义Hook:把路由监听、过滤状态同步、URL更新这些关联逻辑抽成useFilter自定义Hook,让两个组件的职责更单一,减少代码冗余。比如Hook可以封装过滤状态管理、URL参数同步、从URL恢复状态的逻辑。
  2. 简化Reducer逻辑:当前的filterReducer可以简化,用数组方法精简写法,同时用Set避免重复添加过滤项:
    const filterReducer = (state, action) => {
      switch (action.type) {
        case "ADD_FILTER":
          return [...new Set([...state, action.filter])];
        case "REMOVE_FILTER":
          return state.filter(name => name !== action.filter);
        case "ACTIVE_FILTERS":
          return [...action.filterList];
        default:
          return state;
      }
    };
    
  3. 优化URL更新逻辑:
    • 不要直接修改router.query对象,创建新的query对象避免副作用
    • 清空过滤时移除features参数,同时使用shallow: true避免页面重载:
      const filterQuery = filterStates.join("&");
      router.push(
        { query: { ...router.query, features: filterStates.length ? filterQuery : undefined } },
        undefined,
        { shallow: true } // 仅更新路由,不触发页面重新渲染
      );
      
  4. 避免重复计算:每次过滤都调用sortProducts,可以将过滤和排序逻辑分离,或者基于原始props数据排序,而非当前展示的products状态,减少不必要的计算。

二、SEO影响确认

当前方案不会影响SEO,原因如下:

  • 数据通过getStaticProps预渲染,页面源码中已经包含全部商品数据,搜索引擎爬虫可以抓取到完整内容
  • 前端useEffect过滤仅在客户端修改DOM展示内容,不会覆盖预渲染的HTML,爬虫看到的是完整的预渲染数据

如果希望过滤后的页面也能被爬虫抓取,可以考虑两种方案:

  • 启用Incremental Static Regeneration(ISR):配置revalidate参数,让Next.js为不同的过滤参数生成静态页面,兼顾静态页面的性能和动态过滤的需求
  • 使用getServerSideProps:在服务端根据URL查询参数过滤数据后再渲染,但这会失去静态页面的性能优势,需要根据业务流量权衡

内容的提问来源于stack exchange,提问作者weezyBee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 04:27:22