Next.js App Router中useResponsive Hook的Hydration不匹配修复咨询
修复Next.js App Router中MUI useMediaQuery的Hydration不匹配问题
问题原因
服务端渲染阶段没有浏览器环境,useMediaQuery会返回默认的false;客户端Hydration时,实际计算媒体查询结果返回true,导致服务端渲染的DOM和客户端渲染的DOM结构不一致,触发Hydration不匹配错误。
修复方案
方案1:给useMediaQuery添加noSsr选项
MUI的useMediaQuery支持noSsr配置项,启用后会跳过服务端的媒体查询计算,仅在客户端执行,从根源避免服务端和客户端的初始值差异。
修改useResponsive.ts中的每个useMediaQuery调用,添加{ noSsr: true }参数:
import { useMediaQuery, useTheme } from '@mui/material'; import { useEffect, useLayoutEffect } from 'react'; export interface IBreakpointsType { isMobile: boolean; isTablet: boolean; isSmallLaptop: boolean; isLargeLaptop: boolean; isDesktop: boolean; isComputer: boolean; isLaptop: boolean; } const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; const useResponsive = () => { const theme = useTheme(); // 为每个useMediaQuery添加noSsr选项 const isMobile = useMediaQuery(theme.breakpoints.down('sm'), { noSsr: true }); const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg'), { noSsr: true }); const isSmallLaptop = useMediaQuery(theme.breakpoints.between('lg', 'xl'), { noSsr: true }); const isLargeLaptop = useMediaQuery(theme.breakpoints.between('xl', 'xxl'), { noSsr: true }); const isDesktop = useMediaQuery(theme.breakpoints.up('xxl'), { noSsr: true }); const isComputer = useMediaQuery(theme.breakpoints.up('lg'), { noSsr: true }); const isLaptop = useMediaQuery(theme.breakpoints.between('md', 'xl'), { noSsr: true }); useIsomorphicLayoutEffect(() => { const handleResize = () => { window.dispatchEvent(new Event('resize')); }; window.addEventListener('themeChange', handleResize); return () => { window.removeEventListener('themeChange', handleResize); }; }, []); return { isMobile, isTablet, isSmallLaptop, isLargeLaptop, isDesktop, isComputer, isLaptop, }; }; export default useResponsive;
方案2:手动管理客户端状态(兼容复杂场景)
如果方案1无法解决问题,可以通过useState初始化服务端安全值,再在客户端挂载后更新为实际媒体查询结果,确保服务端和客户端初始渲染内容一致。
修改后的useResponsive.ts:
import { useTheme } from '@mui/material'; import { useEffect, useState } from 'react'; export interface IBreakpointsType { isMobile: boolean; isTablet: boolean; isSmallLaptop: boolean; isLargeLaptop: boolean; isDesktop: boolean; isComputer: boolean; isLaptop: boolean; } const useResponsive = () => { const theme = useTheme(); // 初始化服务端安全状态 const [breakpoints, setBreakpoints] = useState<IBreakpointsType>({ isMobile: false, isTablet: false, isSmallLaptop: false, isLargeLaptop: false, isDesktop: false, isComputer: false, isLaptop: false, }); useEffect(() => { // 客户端挂载后计算实际媒体查询值 const updateBreakpoints = () => { setBreakpoints({ isMobile: window.matchMedia(theme.breakpoints.down('sm')).matches, isTablet: window.matchMedia(theme.breakpoints.between('md', 'lg')).matches, isSmallLaptop: window.matchMedia(theme.breakpoints.between('lg', 'xl')).matches, isLargeLaptop: window.matchMedia(theme.breakpoints.between('xl', 'xxl')).matches, isDesktop: window.matchMedia(theme.breakpoints.up('xxl')).matches, isComputer: window.matchMedia(theme.breakpoints.up('lg')).matches, isLaptop: window.matchMedia(theme.breakpoints.between('md', 'xl')).matches, }); }; updateBreakpoints(); // 监听窗口resize和主题变化事件 window.addEventListener('resize', updateBreakpoints); window.addEventListener('themeChange', updateBreakpoints); return () => { window.removeEventListener('resize', updateBreakpoints); window.removeEventListener('themeChange', updateBreakpoints); }; }, [theme]); return breakpoints; }; export default useResponsive;
额外注意事项
- 确保使用
useResponsive的组件(如Footer.tsx)已经添加'use client'指令,明确标记为客户端组件。 - 方案1更简洁,优先使用;方案2适合需要自定义状态更新逻辑的场景。
内容的提问来源于stack exchange,提问作者arunmmanoharan
相关产品推荐
相关产品推荐

