如何在React中基于屏幕尺寸条件显示列表项实现带更多菜单的导航
React 自适应「更多」导航栏实现方案
核心实现逻辑
核心是不依赖固定断点,实时计算导航容器剩余宽度,判断哪些导航项可以显示,哪些放进下拉菜单,相比固定断点适配的兼容性更强,能适配不同内容长度的导航项。
具体实现步骤
- 第一步:用
useRef挂载导航容器、所有导航项、「更多」按钮的DOM引用,提前渲染所有导航项的不可见快照,用来获取每个导航项的实际渲染宽度 - 第二步:计算导航容器总可用宽度,要提前减去「更多」按钮的预留宽度和间距,累加导航项宽度,直到累加值超过可用宽度,临界值之前的导航项作为可见项,之后的全部作为「更多」菜单的下拉项
- 第三步:监听窗口
resize事件,搭配防抖函数优化触发频率,每次尺寸变化都重新计算可见项和下拉项的分组 - 第四步:渲染时区分可见导航项和「更多」下拉菜单,下拉菜单用常规的React状态控制显隐即可
代码示例片段
import { useRef, useState, useEffect } from 'react' // 导航项配置 const navItems = [ { id: '1', label: '首页', path: '/' }, { id: '2', label: '新闻', path: '/news' }, { id: '3', label: '赛事', path: '/games' }, { id: '4', label: '数据', path: '/stats' }, { id: '5', label: '视频', path: '/videos' }, { id: '6', label: '社区', path: '/community' }, // 可继续添加更多导航项 ] const ResponsiveNav = () => { const navContainerRef = useRef<HTMLDivElement>(null) const moreBtnRef = useRef<HTMLButtonElement>(null) const [visibleItems, setVisibleItems] = useState(navItems) const [hiddenItems, setHiddenItems] = useState<typeof navItems>([]) const [isMoreOpen, setIsMoreOpen] = useState(false) // 计算导航项分组的核心函数 const calculateVisibleItems = () => { if (!navContainerRef.current || !moreBtnRef.current) return // 获取容器总宽度 const containerWidth = navContainerRef.current.clientWidth // 获取更多按钮实际宽度 const moreBtnWidth = moreBtnRef.current.offsetWidth // 可用宽度 = 容器宽度 - 更多按钮宽度 - 预留安全间距 const availableWidth = containerWidth - moreBtnWidth - 20 let totalWidth = 0 const tempVisible: typeof navItems = [] const tempHidden: typeof navItems = [] // 获取提前渲染的不可见导航项的宽度 const itemElements = navContainerRef.current.querySelectorAll('.nav-item-snapshot') itemElements.forEach((el, index) => { // 计算单个导航项总宽度(含左右间距) const itemWidth = el.offsetWidth + 16 if (totalWidth + itemWidth <= availableWidth) { totalWidth += itemWidth tempVisible.push(navItems[index]) } else { tempHidden.push(navItems[index]) } }) setVisibleItems(tempVisible) setHiddenItems(tempHidden) } // 防抖函数,避免resize时高频触发计算 const debounce = (fn: Function, delay: number) => { let timer: NodeJS.Timeout return (...args: any[]) => { clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), delay) } } useEffect(() => { calculateVisibleItems() const debouncedCalculate = debounce(calculateVisibleItems, 100) window.addEventListener('resize', debouncedCalculate) return () => window.removeEventListener('resize', debouncedCalculate) }, []) return ( <div ref={navContainerRef} className="nav-container relative flex items-center h-14 px-4 overflow-hidden"> {/* 可见导航项 */} {visibleItems.map(item => ( <a key={item.id} href={item.path} className="mx-2 whitespace-nowrap"> {item.label} </a> ))} {/* 更多按钮,仅存在隐藏项时显示 */} {hiddenItems.length > 0 && ( <div className="relative ml-2"> <button ref={moreBtnRef} onClick={() => setIsMoreOpen(!isMoreOpen)}> 更多 </button> {/* 下拉菜单 */} {isMoreOpen && ( <div className="absolute right-0 top-full w-36 bg-white shadow rounded py-1 z-10"> {hiddenItems.map(item => ( <a key={item.id} href={item.path} className="block px-4 py-2 hover:bg-gray-100"> {item.label} </a> ))} </div> )} </div> )} {/* 不可见的导航项快照,仅用来计算宽度,不参与交互 */} <div className="absolute left-0 top-0 opacity-0 pointer-events-none"> {navItems.map(item => ( <span key={item.id} className="nav-item-snapshot mx-2 whitespace-nowrap"> {item.label} </span> ))} </div> </div> ) } export default ResponsiveNav
注意事项
- 计算宽度时要预留足够的安全边距,避免刚好卡在临界宽度时出现导航项来回抖动的问题
- 如果导航项是动态获取的,需要把导航项列表作为
useEffect的依赖项,导航项更新时重新计算分组 - 服务端渲染场景下,首次渲染可以先默认显示前N个导航项,等客户端 hydration 完成后再重新计算调整,避免 hydration 不匹配报错
内容的提问来源于stack exchange,提问作者billybob2
相关产品推荐
相关产品推荐

