Next.js 13 App Router中媒体查询的无错实现方案
解决Next.js 13 App Router侧边栏响应式渲染的Hydration与window未定义问题
方案1:优先用CSS媒体查询(最推荐,无JS问题)
直接用CSS处理响应式,完全绕开window对象和Hydration问题,这是响应式设计的原生方案,性能更好也更简单。
示例代码:
'use client'; export default function Sidebar() { return ( <div className="sidebar"> {/* 侧边栏内容 */} </div> ); }
对应的CSS(可放在全局样式或组件模块样式中):
/* 大屏样式(≥768px):侧边栏展开 */ .sidebar { width: 250px; transition: width 0.3s ease; display: block; } /* 小屏样式(<768px):侧边栏收起或隐藏 */ @media (max-width: 767px) { .sidebar { width: 60px; /* 如果需要完全隐藏,换成 display: none; */ /* 或者用 transform: translateX(-100%); 实现滑入滑出 */ } }
方案2:用useEffect延迟初始化并监听媒体查询(适合必须用JS控制的场景)
利用useEffect仅在客户端执行的特性,先给服务端一个默认初始状态,客户端挂载后再同步实际媒体查询结果,并监听变化。
示例代码:
'use client'; import { useState, useEffect } from 'react'; export default function Sidebar() { // 服务端渲染时用默认值(比如假设是大屏状态) const [isSidebarOpen, setIsSidebarOpen] = useState(true); useEffect(() => { // 客户端初始化时获取当前媒体查询状态 const mediaQuery = window.matchMedia('(min-width: 768px)'); // 更新为客户端实际的媒体查询结果 setIsSidebarOpen(mediaQuery.matches); // 监听屏幕尺寸变化,实时更新状态 const handleMediaChange = (e) => { setIsSidebarOpen(e.matches); }; mediaQuery.addEventListener('change', handleMediaChange); // 组件卸载时清理监听 return () => mediaQuery.removeEventListener('change', handleMediaChange); }, []); return ( <div className={`sidebar ${isSidebarOpen ? 'open' : 'closed'}`}> {/* 侧边栏内容 */} </div> ); }
方案3:用useSyncExternalStore同步媒体查询状态(React官方推荐的外部状态管理)
如果需要更严谨的状态同步,避免短暂的UI闪烁,可以用React的useSyncExternalStore来订阅媒体查询,确保服务端和客户端初始状态一致。
示例代码:
'use client'; import { useSyncExternalStore } from 'react'; // 获取媒体查询匹配结果,服务端返回默认值 function getMediaQueryMatches(query) { if (typeof window === 'undefined') return true; return window.matchMedia(query).matches; } // 订阅媒体查询变化 function subscribeMediaQuery(query, callback) { const mediaQuery = window.matchMedia(query); mediaQuery.addEventListener('change', callback); return () => mediaQuery.removeEventListener('change', callback); } export default function Sidebar() { // 同步媒体查询状态,服务端和客户端初始值一致 const isLargeScreen = useSyncExternalStore( (callback) => subscribeMediaQuery('(min-width: 768px)', callback), () => getMediaQueryMatches('(min-width: 768px)'), () => getMediaQueryMatches('(min-width: 768px)') ); return ( <div className={`sidebar ${isLargeScreen ? 'open' : 'closed'}`}> {/* 侧边栏内容 */} </div> ); }
关键说明
- 方案1是最优解,因为CSS响应式是浏览器原生支持的,不需要JS介入,完全避免服务端渲染的冲突。
- 方案2和3适合需要结合JS交互逻辑的场景(比如侧边栏手动切换+响应式自动切换),核心是确保服务端和客户端初始渲染的UI一致,再在客户端同步实际状态。
内容的提问来源于stack exchange,提问作者Jaanis
相关产品推荐
相关产品推荐

