Next.js SSR中如何实现滚动时div宽度自动展开与收缩
原有代码问题
- 事件绑定和取值对象不匹配:监听
window滚动事件,却读取目标div自身的scrollTop,如果目标div不是独立滚动容器、是跟随页面整体滚动,它的scrollTop值始终为0,拿不到有效滚动数据 - 逻辑缺失:仅判断了是否滚动到底部,没有做滚动进度的比例映射,无法实现宽度随滚动连续变化的效果
- 闭包与生命周期问题:useEffect依赖项设置为
adjustRef.current,ref值变化不会触发组件重渲染,容易导致事件回调拿到过期的闭包值 - SSR兼容问题:没有做客户端环境判断,服务端渲染阶段直接访问window对象会触发hydration不匹配报错
实现逻辑说明
宽度变化分两个连续阶段,和滚动位置完全绑定:
- 初始状态:目标div顶部刚接触视口顶部时,宽度为90%
- 展开阶段:向下滚动过程中,宽度随滚动进度从90%线性增长到100%,直到div底部距离视口顶部还有一个视口高度的位置
- 收缩阶段:继续向下滚动到div底部离开视口的过程中,宽度从100%线性收回到90%
- 向上滚动时逻辑完全对称,不需要额外编写反向判断,进度值会随滚动方向自动计算
可直接运行的代码实现
适配Next.js SSR模式,添加了性能优化逻辑,避免滚动卡顿:
'use client' import { useState, useRef, useEffect, useCallback } from 'react'; import styles from './YourComponent.module.css'; import MyContent from './MyContent'; export default function AdjustContainer({ myContent }) { const [adjustWidth, setAdjustWidth] = useState('90%'); const adjustRef = useRef(null); const isInViewRef = useRef(false); const handleScroll = useCallback(() => { if (!adjustRef.current || !isInViewRef.current) return; const rect = adjustRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const elementHeight = rect.height; // 元素完全离开视口时终止计算 if (rect.top > viewportHeight || rect.bottom < 0) return; let width; const expandEndPoint = -(elementHeight - viewportHeight); // 第一阶段:宽度从90%展开到100% if (rect.top >= expandEndPoint) { const progress = Math.abs(rect.top) / (elementHeight - viewportHeight); const validProgress = Math.min(Math.max(progress, 0), 1); width = `${90 + validProgress * 10}%`; } else { // 第二阶段:宽度从100%收缩回90% const shrinkRange = viewportHeight; const shrinkProgress = Math.abs(rect.top - expandEndPoint) / shrinkRange; const validProgress = Math.min(Math.max(shrinkProgress, 0), 1); width = `${100 - validProgress * 10}%`; } setAdjustWidth(width); }, []); useEffect(() => { // 跳过服务端执行阶段,避免window访问报错 if (typeof window === 'undefined') return; // 用IntersectionObserver判断元素是否在视口,减少无效计算 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { isInViewRef.current = entry.isIntersecting; if (entry.isIntersecting) handleScroll(); }); }, { threshold: 0 }); if (adjustRef.current) observer.observe(adjustRef.current); // 绑定滚动事件,加passive优化滚动性能 window.addEventListener('scroll', handleScroll, { passive: true }); // 初始化时计算一次初始宽度 handleScroll(); return () => { observer.disconnect(); window.removeEventListener('scroll', handleScroll); }; }, [handleScroll]); return ( <div className={styles.AdjustContainer} ref={adjustRef} style={{ width: adjustWidth, transition: 'width 0.1s linear', margin: '0 auto' }} > {myContent.map((module, i) => ( <MyContent {...module} key={i} /> ))} </div> ); }
调优说明
- 滚动事件添加passive: true配置,不阻塞主线程,保证滚动流畅
- 所有进度值都做了0-1的边界截断,宽度始终控制在90%-100%区间,不会出现异常值
- 元素不在视口范围内时终止计算,减少不必要的性能消耗
- 宽度过渡时长可以根据实际交互手感调整,建议不超过0.2s,避免滚动和宽度变化不同步
- 所有浏览器API访问都放在客户端useEffect生命周期内,完全兼容SSR模式,不会出现hydration报错
内容的提问来源于stack exchange,提问作者peakyBlinders
相关产品推荐
相关产品推荐

