在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;
优化说明
- 分离ref集合:新增
fullScreenProductRefs专门存储全屏列表的产品元素,和网格视图元素彻底区分,避免混淆 - 使用原生scrollIntoView:替代手动计算偏移,通过
block: 'center'直接实现垂直居中滚动,代码更简洁且兼容性更好 - 完善ref管理:赋值ref时处理元素卸载情况(删除对应ref),避免内存泄漏
- 简化网格视图代码:网格视图不需要存储ref,因为滚动逻辑只和全屏列表有关,去掉无用的
containerRef和productRefs
内容的提问来源于stack exchange,提问作者Garrett Rozsa
相关产品推荐
相关产品推荐

