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

在Next.js+Tailwind中用useEffect与useRef实现产品选中后自动滚动

解决Next.js+Tailwind电商页面点击产品后全屏列表自动滚动到目标项的问题

我正在用Next.js和Tailwind CSS开发电商页面,想要实现类似Instagram的交互:用户点击网格里的产品图片后,页面切换为占据整个视口的FeaturedProduct卡片列表(顺序和网格视图一致),同时需要让选中的产品自动滚动到视图中间。目前组件功能基本完成,但自动滚动的逻辑没实现对,我对useRef和useEffect的使用不太熟,希望有更优雅的解决方案。

现有代码:

import React, { useContext, useState, useEffect, useRef } from 'react';
import FeaturedProduct from '@/components/featured-product';
import { ProductsContext } from '@/context/products-context';

const Home = () => {
  const [selectedProduct, setSelectedProduct] = useState(null);
  const products = useContext(ProductsContext);

  const containerRef = useRef(null);
  const productRefs = useRef([]);

  const handleProductClick = (product) => {
    setSelectedProduct(product);
  };

  useEffect(() => {
    console.log('useEffect called');

    if (selectedProduct && productRefs.current[selectedProduct.id]) {
      const productElement = productRefs.current[selectedProduct.id];
      const containerElement = containerRef.current;
      if (productElement && containerElement) {
        const containerRect = containerElement.getBoundingClientRect();
        const productRect = productElement.getBoundingClientRect();
        const offset = productRect.top - containerRect.top - (containerRect.height - productRect.height) / 2;

        containerElement.scrollBy({
          top: offset,
          behavior: 'auto',
        });
      }
    }
  }, [selectedProduct]);

  const renderProductList = () => {
    return (
      <div className='grid grid-cols-3 gap-4' ref={containerRef}>
        {products.map((product, index) => (
          <div 
          key={product.id} 
          ref={(el) => (productRefs.current[product.id] = el)}
          onClick={() => handleProductClick(product)}
          >
            <img
            src={product.imageUrl}
            alt={product.title} 
            className='w-full h-auto cursor-pointer'
            />
          </div>
        ))}
      </div>
    );
  };

  const renderFullScreenProduct = () => {
    if (!selectedProduct) return null;

    const selectedProductIndex = products.findIndex((product) => product.id === selectedProduct.id);

    const allProducts = products.map((product, index) => (
    <div key={product.id} ref={(el) => index === selectedProductIndex && el}>
      <FeaturedProduct { ...product } key={product.id} />
    </div>
  ));

    return (
      <div className='fixed top-0 left-0 w-full h-full overflow-y-auto bg-white'>
        <div className='max-w-screen-md mx-auto p-4' >
          {allProducts}
          <button
            className='absolute top-4 right-4 text-gray-600'
            onClick={() => setSelectedProduct(null)}
          >
            Close
          </button>
        </div>
      </div>
    );
  };

  return (
    <div className='mx-4'>
      {selectedProduct ? renderFullScreenProduct() : renderProductList()}
    </div>
  );
};

export default Home;

问题分析

你的代码存在几个关键问题导致自动滚动失效:

  • productRefs存储的是网格视图里的产品元素,而非全屏列表里的FeaturedProduct容器,切换到全屏后用这些元素计算滚动偏移完全不对
  • 全屏列表里的选中产品容器虽然加了ref,但没有存到可访问的ref集合里,无法获取对应的DOM元素
  • 手动计算滚动偏移的方式繁琐且易出错,浏览器原生API就能更简洁实现居中滚动

优化后的代码

import React, { useContext, useState, useEffect, useRef } from 'react';
import FeaturedProduct from '@/components/featured-product';
import { ProductsContext } from '@/context/products-context';

const Home = () => {
  const [selectedProduct, setSelectedProduct] = useState(null);
  const products = useContext(ProductsContext);

  // 全屏列表的容器ref
  const fullScreenContainerRef = useRef(null);
  // 存储全屏列表中每个产品的ref,用product.id作为键
  const fullScreenProductRefs = useRef({});

  const handleProductClick = (product) => {
    setSelectedProduct(product);
  };

  useEffect(() => {
    if (!selectedProduct) return;

    // 获取全屏列表中对应产品的DOM元素
    const targetElement = fullScreenProductRefs.current[selectedProduct.id];
    if (targetElement && fullScreenContainerRef.current) {
      // 使用scrollIntoView实现居中滚动,浏览器自动处理偏移计算
      targetElement.scrollIntoView({
        behavior: 'smooth', // 平滑滚动,可选'auto'
        block: 'center', // 垂直方向居中
        inline: 'center' // 水平方向居中(产品居中布局时可按需调整)
      });
    }

    // 清理函数:组件卸载或selectedProduct变化时,清空无用的ref
    return () => {
      if (selectedProduct.id) {
        delete fullScreenProductRefs.current[selectedProduct.id];
      }
    };
  }, [selectedProduct]);

  const renderProductList = () => {
    return (
      <div className='grid grid-cols-3 gap-4'>
        {products.map((product) => (
          <div 
            key={product.id} 
            onClick={() => handleProductClick(product)}
            className='cursor-pointer'
          >
            <img
              src={product.imageUrl}
              alt={product.title} 
              className='w-full h-auto'
            />
          </div>
        ))}
      </div>
    );
  };

  const renderFullScreenProduct = () => {
    return (
      <div className='fixed top-0 left-0 w-full h-full overflow-y-auto bg-white'>
        <div 
          className='max-w-screen-md mx-auto p-4' 
          ref={fullScreenContainerRef}
        >
          {products.map((product) => (
            <div 
              key={product.id} 
              ref={(el) => {
                // 正确赋值ref:存在则存储,不存在则删除(避免内存泄漏)
                if (el) {
                  fullScreenProductRefs.current[product.id] = el;
                } else {
                  delete fullScreenProductRefs.current[product.id];
                }
              }}
            >
              <FeaturedProduct {...product} />
            </div>
          ))}
          <button
            className='absolute top-4 right-4 text-gray-600 hover:text-gray-900'
            onClick={() => setSelectedProduct(null)}
          >
            Close
          </button>
        </div>
      </div>
    );
  };

  return (
    <div className='mx-4'>
      {selectedProduct ? renderFullScreenProduct() : renderProductList()}
    </div>
  );
};

export default Home;

优化说明

  1. 分离ref集合:新增fullScreenProductRefs专门存储全屏列表的产品元素,和网格视图元素彻底区分,避免混淆
  2. 使用原生scrollIntoView:替代手动计算偏移,通过block: 'center'直接实现垂直居中滚动,代码更简洁且兼容性更好
  3. 完善ref管理:赋值ref时处理元素卸载情况(删除对应ref),避免内存泄漏
  4. 简化网格视图代码:网格视图不需要存储ref,因为滚动逻辑只和全屏列表有关,去掉无用的containerRef和productRefs

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 02:57:13