混音水合失败:服务器上的UI和客户端不匹配

发布于 2025-02-10 07:09:10 字数 3935 浏览 3 评论 0原文

在本地很好(已知警告和CSS呈现很好),但是在Vercel上,我的Remix应用程序会出现此错误:

水合失败,因为初始UI与什么不匹配 在服务器上渲染。

商业逻辑运行良好,但CSS完全破坏了。

更新2022年6月26日,15:50

我从头开始启动了一个新项目,并逐个添加了依赖关系,并在每个步骤中部署到Vercel。没有错误。样式的组件呈现良好。因此,依赖关系不是问题。

然后,我开始通过加载程序从数据库中获取数据,然后将它们呈现为一个样式的组件。一致打破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

使用上面相同的最简单项目,我和我已经确定,当我们尝试渲染小时时,具有样式组件的CSS会破裂,即:

const hr = now.getHours();

<DateTimeSpan>
  {hr}
</DateTimeSpan>

我们的怀疑是样式的组件,因为小时是因为时间而断断续续在服务器上的UTC时间呈现,但在客户端上的位置时间。

我不确定这是否是一个错误,还是我们应该自己处理。还不确定是否应该在混音或样式的组件GitHub问题上询问此问题。我已经打开了问题无论如何也可以开始混音。

原始帖子

不确定,但可能与这些问题有关:

我阅读了以上内容和其他几页,我想做的就是更新一些依赖项。这可能是可能的:

{
"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",
}

我的主要怀疑是与样式组件有关,因为我以前在NextJ上也有类似的问题。但是我的app/root.tsx和app/enter.server.tsx关注此示例对于样式组件非常紧密:

// 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,
  });
}

与该示例的最大区别似乎是,我不使用hydrate,我使用hydrateoot REACT 18。不确定它是否与问题有关:

// app/entry.client.tsx

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

hydrateRoot(document, <RemixBrowser />);

CSS-In-JS库上的混音说:“您可以在使用样式的组件时遇到水合警告/essugy/3660“ rel =“ noreferrer”>此问题将很快解决。”这个问题尚未解决,因此也许这个问题还没有解决方案。

但是,如果示例回购有效,那么也许我错过了什么?

It's fine locally (known warning and CSS renders well), but on Vercel my Remix app gets this error:

Hydration failed because the initial UI does not match what was
rendered on the server.

Business logic runs fine but CSS is utterly broken.

Update 26 June 2022, 15:50

I started a new project from scratch and added dependencies one by one and deploying to Vercel at each step. No errors. Styled components render well. So the dependencies are not the problem.

I then started fetching data piece by piece from my database through the loader and rendering them in styled components one by one. The one thing that consistently breaks the CSS and produces the error is the conversion of a datetime object to a string before rendering:

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

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

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

Curiously, it's only when I format it to render only time that it breaks. With date, it's fine:

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

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

I'm at a loss to explain this.

Update 2 July 2022, 21:55

Using the same simplest project above, friend and I have determined that CSS with styled components breaks when we try to render hours, i.e.:

const hr = now.getHours();

<DateTimeSpan>
  {hr}
</DateTimeSpan>

Our suspicion is styled components breaks because hours is rendered in UTC time on the server but locale time on the client.

I'm not sure if this is a bug or if we're supposed to handle this ourselves. Also not sure if this should be asked on Remix or Styled components GitHub issues. I've opened an issue on Remix as well anyway for a start.

Original post

Not sure but could be related to these issues:

I read through the above and a few other pages and all I could think to do was update some dependencies. Here are the possibly relevant ones:

{
"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",
}

My main suspicion is it has to do with styled-components since I've had similar issues on Nextjs before. But my app/root.tsx and app/entry.server.tsx follow this example for styled-components very closely:

// 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,
  });
}

The biggest difference with the example seems to be that instead of using hydrate, I use hydrateRoot as we should for React 18. Not sure if it has any bearing on the problem:

// app/entry.client.tsx

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

hydrateRoot(document, <RemixBrowser />);

The Remix docs on CSS-in-JS libraries says: "You may run into hydration warnings when using Styled Components. Hopefully this issue will be fixed soon." The issue hasn't been resolved so maybe this problem doesn't have a solution yet.

But if the example repo works, then maybe I missed something?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

战皆罪 2025-02-17 07:09:11

是的,尤其是小时数是问题,因为服务器时间在UTC中,并且客户端时间是所在的时间(UTC + X小时)。这会导致UI在两者上都不同。

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

export TZ=UTC

npm run dev

我们将看到CSS如上所述如上所述。

有几种解决此问题的方法,特定于不同的用例。一种是不发送DateTime对象。而是将其作为字符串发送。例如:

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(查看页面源,我们不会看到正确渲染的日期)。如果我们不这样做,机器人将无法正确索引应用程序。

Yes, rendering hours in particular is the issue, since server time is in UTC and client time is whatever the locale time is (UTC + X hours). This causes UI to be different on both.

One quick way to check this out is by setting the timezone for our current CLI instance to UTC before running the app and trying out its pages:

export TZ=UTC

npm run dev

We'll see that the CSS breaks as described in the problem above.

There're several ways to fix this, specific to different use cases. One is to not send a datetime object. Instead, send it as a string. For instance:

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.

This assumes that we already know the client's time zone so we can use that to set/format time on the server.

A more flexible way is to set the time only after the page has mounted. For instance:

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",
          })}</>

With this solution, we lose some of the benefit of SSR. This has implications. For instance, we'll need to pay special attention to SEO (view page source, we won't see the date properly rendered). Robots won't be able to index the app properly if we don't.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文