页面上的错误 class 名称在客户端呈现的内容与在服务器呈现的内容不同

wrong class names on pages with different content rendered on client than on server

我一直在将我的 React + material-ui SPA 切换到 Next.js 静态呈现的站点(下一次导出)。我已按照 material-ui 示例中显示的步骤使用 next.js 并且在非移动屏幕宽度(> 960)上一切正常,但内容在初始显示时未设置样式如果初始渲染时的屏幕宽度等于或低于移动断点,则渲染。随后导航到客户端上的任何页面都会正确呈现页面,即使在导航回初始呈现时损坏的原始违规页面时也是如此,同样这仅适用于移动屏幕宽度。

在我的代码中有很多这样的:

...
const windowWidth = useWindowWidth();
const isMobile = windowWidth < 960;
return (
    // markup
    { isMobile ? (...) : (...) }
    // more markup
);
...

其中 useWindowWidth.js 这样做:

function useWindowWidth() {
  const isClient = typeof window === "object";
  const [width, setWidth] = useState(isClient ? window.innerWidth : 1000); // this will be different between server and client
  useEffect(() => {
    setWidth(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
  return width;
}

当初始渲染在移动屏幕宽度范围内完成时,任何具有此警告的页面都会在控制台中显示此警告:

Warning: Expected server HTML to contain a matching <div> in <div> // or something similar depending on what was conditionally rendered with isMobile

只有这些页面存在此 css 样式问题。似乎当有条件渲染时在该屏幕宽度内渲染这些页面会创建具有不同名称的样式,因此元素调用将只有 makeStyles-button-97 而不是 makeStyles-button-96 class让元素保持无样式。

我已经完成了 material-ui 问题和文档,并确保我的项目合理地反映了示例。包括 _document.js_app.js 文件。我该如何补救?

PS:

我记得在我的搜索中读到过一些内容,其中指出 React 期望服务器和客户端呈现的输出匹配,但如果没有办法解决它,则可以通过某种方式在代码中表示这一点。我不确定这是否只会消除警告,或者是否会完全阻止 class 重命名。有人可以对此有所了解吗?我好像找不到我在哪里读到的...

确定的问题:

要清楚,服务器和客户端之间的 window 宽度差异是这里的违规者。在上面显示的 useWindowWidth 挂钩中,将默认值设置为低于 960 移动阈值,如下所示:

const isClient = typeof window === "object";
const [width, setWidth] = useState(isClient ? window.innerWidth : 900); // change the default to 900 if not on client, so below the mobile threshold

使我的问题相反。因此,在移动屏幕宽度上的初始加载很好,但更大的屏幕宽度会破坏 css 不匹配的 class 名称。是否有推荐的方法根据屏幕宽度有条件地渲染,以某种方式保持输出相同?

更新:

虽然我已经找到了修复方法,正如我自己在下面的回答中所述,但我对此并不满意,并且想更好地了解这里发生的事情,以便我可以在 build 时间解决这个问题而不是解决问题而不是预防问题的解决方案。在这一点上,任何为我指明正确方向的答案都将被接受。

对于有类似问题的任何人,这是我找到的修复方法。不过,我不得不说,这感觉像是一个 hack,而不是解决根本问题。

Material UI: Styles flicker in and disappear

基本上,张贴者 (@R R) 通过更改其 [=23= 中的元素中的关键道具,在客户端上强制使用具有挂载效果的 refresh/re-render 来解决问题。 ] 文件。

虽然这确实修复了样式,但我认为更简洁的解决方案可以在构建时解决问题。如果有人知道如何在构建时解决这个问题,或者至少可以阐明在哪里寻找问题,或者甚至只是对 Next 的构建时机制提供有关样式解决的解释,那将不胜感激。

我的猜测是,通过问题中概述的条件呈现,移动设备和其他屏幕宽度之间呈现的内容的差异导致了 class 名称的某种分支。至少那是控制台中记录的警告会让我相信的。我仍然找不到我在问题中提到的那篇文章,讨论了该警告和解决它的方法(无论是只是消除警告,还是更重要的是,完全防止不匹配的 class 名称)。如果有人对 article/blog/site 有 link,我将不胜感激。

您可以尝试 material ui 的 useMediaQuery 挂钩,这将给出 window 的宽度,并在其更改时进行更新。如果您需要自定义断点也可以在主题中更新

import withWidth from '@material-ui/core/withWidth';

function MyComponent({width}) {
  
 const isMobile = (width === 'xs');
 return (
    // markup
   { isMobile ? (...) : (...) }
   // more markup
   );

export default withWidth()(MyComponent);

对于自定义断点,您可以这样尝试

const theme = createMuiTheme({
  breakpoints: {
    values: {
     mobile: 540,
     tablet: 768,
     desktop: 1024,
    },
  },
})

您可以使用 next/dynamic with { ssr: false } as described here。基本上就是把这个问题的相关代码隔离到自己的组件中,然后在关闭ssr的情况下动态导入。这避免了加载需要 window 服务器端的特定代码。

您还可以在动态获取组件时使用自定义加载组件as described here

or even just provide an explanation on Next's build time mechanics where styling is addressed, that would be greatly appreciated.

问题基本上归结为在ssr期间没有window对象存在是我的基本理解。对于我正在使用的 Bootstrap 旋转木马,我遇到了与您类似的问题,我认为动态导入是我将要使用的 - 这个解决方案允许我们根本不修改我们的代码ssr,除了简单的隔离相关代码。

来自ReactDOM.hydrate documentation

React expects that the rendered content is identical between the server and the client.

但是通过在初始渲染期间利用 window.innerWidth 进行以下操作:

const [width, setWidth] = useState(isClient ? window.innerWidth : 1000);

只要宽度导致的渲染与宽度 1000 导致的渲染不同(例如,当它在你的分支上的代码示例中小于 960 时,你就会导致初始客户端渲染与服务器不同isMobile)。这可能会导致各种水合作用问题,具体取决于基于宽度的分支导致的差异类型。

我认为您应该能够通过将 useState 初始化简化为硬编码来解决此问题 1000:

function useWindowWidth() {
  const isClient = typeof window === "object";
  const [width, setWidth] = useState(1000);
  useEffect(() => {
    setWidth(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
  return width;
}

效果已经在无条件调用 setWidth(window.innerWidth),如果需要(例如移动设备),它应该负责在初始渲染后更新布局。

如果您从未使用过特定宽度,而只是将其用作分支的阈值,我建议您可靠地使用 Material-UI 的 useMediaQuery instead of your custom useWindowWidth hook. The documentation then explains ways of dealing with server-side rendering。在您的情况下(使用下一个导出),您可以使用更简单的服务器端 ssrMatchMedia 实现,它始终假定 1024px 而不是包括用户代理解析以​​尝试检测设备类型。

除了处理 SSR 问题外,useMediaQuery 将触发较少的宽度更改重新渲染,因为它只会在 window 大小超过指定媒体查询的阈值时触发渲染比赛。

相关回答: