Remix水合失败:服务器和客户端的UI不匹配。

11

在本地(已知警告和CSS呈现良好)运行正常,但在Vercel上,我的Remix应用程序出现以下错误:

由于初始UI与服务器上呈现的不匹配,因此水合作业失败。

业务逻辑运行正常,但CSS完全失效。

更新2022年6月26日15:50

我从头开始新建了一个项目,并逐步添加依赖项,每一步都在Vercel上部署。没有错误。Styled组件呈现良好。因此,依赖关系不是问题所在。

然后,我逐个从我的数据库中通过加载器获取数据,并将它们渲染到Styled组件中。唯一一件始终破坏CSS并产生错误的事情是在呈现之前将datetime对象转换为字符串:

const DateTimeSpan = styled.span`
  font-size: 1rem;
`;

const hr = now.getHours();
const min = now.getMinutes();

<DateTimeSpan>
  {`${hr}:${min}`}
</DateTimeSpan>

奇怪的是,只有当我将其格式化为仅呈现时间时,它才会出错。使用日期则没有问题:

const yr = now.getFullYear();
const mth = now.getMonth();
const dd = now.getDate();

<DateTimeSpan>
  {`${yr}-${mth}-${dd}`}
<DateTimeSpan>

我无法解释这个问题。

2022年7月2日更新,21:55

在使用上述最简项目时,我和朋友发现使用styled components的CSS在尝试渲染小时数时会出现问题:

const hr = now.getHours();

<DateTimeSpan>
  {hr}
</DateTimeSpan>

我们怀疑 styled components 出现问题是因为在服务器上以 UTC 时间呈现,但在客户端以本地时间呈现。

我不确定这是否是一个 bug,或者我们是否应该自己处理这个问题。也不确定是否应该在 Remix 或 Styled components 的 GitHub 问题中提出此问题。无论如何,我已经在 Remix 上开了一个issue

原始帖子

不确定是否与以下问题相关:

我阅读了以上和其他几个页面,所有我能想到的就是更新一些依赖项。以下是可能相关的依赖项:

{
"react": "^18.2.0",
"styled-components": "^5.3.5"
"@remix-run/node": "^1.6.1",
"@remix-run/react": "^1.6.1",
"@remix-run/vercel": "^1.6.1",
"@vercel/node": "^2.2.0",
}

我主要怀疑与styled-components有关,因为我之前在Nextjs上也遇到过类似的问题。但是我的app/root.tsx和app/entry.server.tsx非常接近这个例子中关于styled-components的方式:

// app/root.tsx

export default function App() {
  const data = useLoaderData();

  return (
    <Html lang="en">
      <head>
        ...
        {typeof document === "undefined" ? "__STYLES__" : null}
      </head>
      <Body>
        ...
      </Body>
    </Html>
  );
}

//app/entry.server.tsx

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const sheet = new ServerStyleSheet();

  let markup = renderToString(
    sheet.collectStyles(
      <RemixServer context={remixContext} url={request.url} />
    )
  );
  const styles = sheet.getStyleTags();
  markup = markup.replace("__STYLES__", styles);
  responseHeaders.set("Content-Type", "text/html");

  return new Response("<!DOCTYPE html>" + markup, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
}

与示例最大的不同似乎是,我使用hydrateRoot而不是hydrate,这是React 18应该使用的方式。不确定它是否对问题有影响:

// app/entry.client.tsx

import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";

hydrateRoot(document, <RemixBrowser />);
Remix文档关于CSS-in-JS库中提到:“使用Styled Components时可能会遇到水合警告。希望这个问题很快就会被解决。” 该问题目前尚未解决,因此可能还没有解决方案。

但是,如果示例存储库有效,则可能我错过了什么?


如果我要猜的话,可能是因为你尝试打印的变量是 now;它包含时间(毫秒),在服务器端和客户端渲染之间会发生变化。服务器上的 now 与客户端上的 now 不同。 - boop_the_snoot
这就解释了为什么我只在晚上遇到这个错误。真是太烦人了。感谢您花时间仔细记录这个问题。 - Jon Crowell
问题写得很好,而且一路上有良好的更新。 - MEMark
1个回答

3

是的,具体来说渲染小时数是个问题,因为服务器时间是UTC格式,而客户端时间则是本地时间(UTC + X小时)。这将导致用户界面上的显示不同。

一个快速检查的方法是,在运行应用程序之前将当前CLI实例的时区设置为UTC,并尝试其页面:

export TZ=UTC

npm run dev

我们将看到CSS如上述问题一样出现问题。
有几种不同的方法可以解决这个问题,具体取决于不同的使用情况。其中一种方法是不要发送日期时间对象,而是将其作为字符串发送。例如:
const now: Date = new Date()

// Locale time as example only, we need to know client's locale time
const time: string = now.toLocaleTimeString([], {
               hour: "2-digit",
               minute: "2-digit",
             })

// Send time string to client.

这假设我们已经知道客户端所在的时区,因此我们可以使用该时区设置/格式化服务器上的时间。

一种更灵活的方法是仅在页面装载后设置时间。例如:

const [now, setNow] = useState<Date>();
const loaderData = useLoaderData<string>();

useEffect(() => {
  if (!loaderData) {
    return;
  }

  setNow(JSON.parse(loaderData));
}, [loaderData]);

return <>{now.toLocaleTimeString([], {
            hour: "2-digit",
            minute: "2-digit",
          })}</>

使用这种解决方案,我们会失去一些SSR的好处。这对应用程序有影响。例如,我们需要特别关注SEO(查看页面源代码,我们将无法正确渲染日期)。如果我们不这样做,机器人将无法正确索引应用程序。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接