使用Material UI与ReactDOMServer时如何修复「Warning: useLayoutEffect does nothing on the server」警告?
我完全懂你的烦恼——功能都正常,但控制台这个警告一直跳出来确实闹心,而且还不想去掉ThemeProvider(毕竟主题样式肯定不能丢)。下面给你几个可行的优化方案,不用砍掉ThemeProvider也能解决问题:
为什么会出现这个警告?
简单说,Material UI的ThemeProvider内部用到了useLayoutEffect钩子,但ReactDOMServer的渲染环境(哪怕你是在客户端调用这个方法)不会执行useLayoutEffect,React就会警告你:服务器渲染的输出和客户端最终渲染的UI可能会有差异,毕竟这个钩子的效果没法编码到服务器输出里。
方案一:改用客户端渲染生成HTML(推荐,适合你的场景)
既然你是在客户端组件里生成Leaflet的DivIcon HTML,完全没必要用ReactDOMServer的方法,直接用客户端的ReactDOM.render渲染到临时DOM节点,再提取innerHTML就行。这样useLayoutEffect能正常执行,警告自然消失:
import ReactDOM from 'react-dom'; const mapIcon = useMemo(() => { let namePosition: MapPinProps['namePosition'] = 'bottom-start'; if (position.lng > 120) { namePosition = 'bottom-end'; } if (position.lng > 120 && position.lat < -80) { namePosition = 'top-end'; } if (position.lng <= 120 && position.lat < -80) { namePosition = 'top-start'; } // 创建临时DOM节点 const tempDiv = document.createElement('div'); // 渲染组件到临时节点 ReactDOM.render( <ThemeProviders> <MapPin active={isMapBuilder ? active : !completed} highlight={highlightRequiredEvent} icon={completed && doneIcon ? doneIcon : icon} name={event?.type !== EVENTS_TYPE.ANIMATION || isMapBuilder ? name : ''} rarity={rarity} read={isMapBuilder || event?.type === EVENTS_TYPE.ANIMATION || read} interactive={isMapBuilder || event?.type !== EVENTS_TYPE.ANIMATION} selected={selected} shape={shape} size={iconSize} userSettings={user.settings} namePosition={namePosition} locked={locked && !isMapBuilder} isMapBuilder={isMapBuilder} /> </ThemeProviders>, tempDiv ); // 提取HTML字符串 const html = tempDiv.innerHTML; // 清理临时节点,避免内存泄漏 ReactDOM.unmountComponentAtNode(tempDiv); return new L.DivIcon({ className: '', iconSize: [iconSize, iconSize], html: html, }); }, [ position.lng, position.lat, event?.type, isMapBuilder, active, completed, highlightRequiredEvent, doneIcon, icon, name, rarity, read, selected, shape, iconSize, user.settings, locked, ]);
这个方法的好处是完全贴合客户端环境,不会有SSR相关的钩子冲突,样式也能正常应用。
方案二:用Material UI官方SSR工具兼容
如果你坚持要用ReactDOMServer,可以用Material UI专门的ServerStyleSheet来收集样式,同时处理钩子的兼容性问题:
import { ServerStyleSheet } from '@mui/material/styles'; const mapIcon = useMemo(() => { let namePosition: MapPinProps['namePosition'] = 'bottom-start'; if (position.lng > 120) { namePosition = 'bottom-end'; } if (position.lng > 120 && position.lat < -80) { namePosition = 'top-end'; } if (position.lng <= 120 && position.lat < -80) { namePosition = 'top-start'; } const sheet = new ServerStyleSheet(); let html = ''; try { // 用sheet.collectStyles包裹ThemeProvider,收集样式 html = ReactDOMServer.renderToStaticMarkup( sheet.collectStyles( <ThemeProviders> <MapPin active={isMapBuilder ? active : !completed} highlight={highlightRequiredEvent} icon={completed && doneIcon ? doneIcon : icon} name={event?.type !== EVENTS_TYPE.ANIMATION || isMapBuilder ? name : ''} rarity={rarity} read={isMapBuilder || event?.type === EVENTS_TYPE.ANIMATION || read} interactive={isMapBuilder || event?.type !== EVENTS_TYPE.ANIMATION} selected={selected} shape={shape} size={iconSize} userSettings={user.settings} namePosition={namePosition} locked={locked && !isMapBuilder} isMapBuilder={isMapBuilder} /> </ThemeProviders> ) ); // 把收集到的样式内联到HTML开头,确保样式生效 const styleElements = sheet.getStyleElement(); html = `${styleElements.innerHTML}${html}`; } finally { sheet.seal(); } return new L.DivIcon({ className: '', iconSize: [iconSize, iconSize], html: html, }); }, [ position.lng, position.lat, event?.type, isMapBuilder, active, completed, highlightRequiredEvent, doneIcon, icon, name, rarity, read, selected, shape, iconSize, user.settings, locked, ]);
这个方法能让Material UI的样式在SSR渲染时正确收集,同时减少钩子不匹配的警告(不过部分版本可能还会有提示,但至少样式不会出问题)。
方案三:临时抑制警告(不推荐,仅应急用)
如果上面的方案都暂时没法用,你可以临时抑制这个特定警告,但要注意这只是隐藏问题,不是解决问题:
// 在渲染前保存原console.warn const originalWarn = console.warn; // 过滤掉特定的useLayoutEffect警告 console.warn = (...args) => { if (typeof args[0] === 'string' && args[0].includes('useLayoutEffect does nothing on the server')) { return; } originalWarn(...args); }; // 执行你的ReactDOMServer渲染逻辑 const html = ReactDOMServer.renderToStaticMarkup( <ThemeProviders> <MapPin {/* 你的props */} /> </ThemeProviders> ); // 恢复原console.warn console.warn = originalWarn;
不推荐长期用这个方法,因为可能错过其他重要的警告信息。
内容的提问来源于stack exchange,提问作者Laura Grassi

