You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Material UI与ReactDOMServer时如何修复「Warning: useLayoutEffect does nothing on the server」警告?

解决ReactDOMServer + Material UI ThemeProvider的useLayoutEffect警告问题

我完全懂你的烦恼——功能都正常,但控制台这个警告一直跳出来确实闹心,而且还不想去掉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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 01:42:39