Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error in Dynamic Import example #487

Closed
arthurgailes opened this issue Oct 25, 2022 · 20 comments
Closed

Error in Dynamic Import example #487

arthurgailes opened this issue Oct 25, 2022 · 20 comments
Labels

Comments

@arthurgailes
Copy link
Contributor

arthurgailes commented Oct 25, 2022

Description

When using the example on dynamic loading with a React.Suspense component, I receive the following error: Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.

The page still works, but I'm assuming I'm losing some SSR benefit here. I believe this is resulting from the components in node and the browser being different - even when they call the same underlying function. As an example, the code below produces the same error, adapted from the example:


// pages/pick-location.page.jsx

import React from 'react'
const Loading = () => <div>Loading...</div>;

export function Page() {
  return <>
    Please pick a location.
    <Map />
  </>
}

function Map() {
  const isBrowser = typeof window !== 'undefined'
  const isNodejs = !isBrowser

  // If I delete this section, the error disappears
  if (isNodejs) {
    return <Loading />
  }

  // Removing the lazy load and Suspense to illustrate the problem
  return (
    <div>A different div</div>
  )
}

Error Stack

react-dom.development.js:19849 Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
    at updateHostRoot (react-dom.development.js:19849:57)
    at beginWork (react-dom.development.js:21615:14)
    at beginWork$1 (react-dom.development.js:27426:14)
    at performUnitOfWork (react-dom.development.js:26557:12)
    at workLoopSync (react-dom.development.js:26466:5)
    at renderRootSync (react-dom.development.js:26434:7)
    at recoverFromConcurrentError (react-dom.development.js:25850:20)
    at performConcurrentWorkOnRoot (react-dom.development.js:25750:22)
    at workLoop (scheduler.development.js:266:34)
    at flushWork (scheduler.development.js:239:14)
@brillout
Copy link
Member

This doesn't seem related to vps, see https://brillout.github.io/rules/. Feel free to object.

@arthurgailes
Copy link
Contributor Author

arthurgailes commented Oct 25, 2022

I don't understand your statement. At a bare minimum, it's related to the VPS docs, as the provided example doesn't work properly.

From a broader point of view, this appears to be the only way of achieving client-only components while also using SSR/SSG (not the SPA page.client.* formatting) - if VPS can't be used this way, does that mean that VPS is invalid for all such use cases?

Edit: I've further simplified the example to verify that React.Suspense is not the root of the problem.

@brillout
Copy link
Member

It's a user land thing: you're supposed to use React.lazy.

@arthurgailes
Copy link
Contributor Author

arthurgailes commented Oct 26, 2022

No, I only removed that to simplify the example. I've created a fork with an example of the problem: main...arthurgailes:vite-plugin-ssr:ssr-client-fix

The main code:

export { Page }

import './index.css'

const Loading = () => <div>Loading</div>
function Page() {
  return (
    <>
      <h1>SSR</h1>
      <p>This page is:</p>
      <ul>
        <li>Rendered to HTML and hydrated in the browser.</li>
        <li>
          Interactive. <ClientSideComponent />
        </li>
      </ul>
      <p className="colored ssr">Blue text.</p>
    </>
  )
}

function ClientSideComponent() {
  const isBrowser = typeof window !== 'undefined'
  const isNodejs = !isBrowser

  // We lazily load the client-side component
  if (isNodejs) {
    return <Loading />
  }

  // We lazily load the client-side component
  const Counter = React.lazy(() => import('./Counter'))
  return (
    <React.Suspense fallback={<Loading />}>
      <Counter />
    </React.Suspense>
  )
}

@brillout
Copy link
Member

I'm actually rewriting the docs as we speak.

@brillout
Copy link
Member

Alright, here it is: https://vite-plugin-ssr.com/dynamic-import.

Apologies for the subpar docs about this. Hope it's better now.

Get rid of branches like if (isNodejs) and that should fix you problem.

@arthurgailes
Copy link
Contributor Author

Thanks very much. Unfortunately, this opens a new can of worms. I've updated the fork here: main...arthurgailes:vite-plugin-ssr:ssr-client-fix

The new error message: "Uncaught Error: The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server"

Then, if I switch to renderToPipeableStream, I get a new error on the server: "[Wrong Usage][dangerouslySkipEscape(str)] Argument str should be a string but we got typeof str === "object"."

I'm not familiar enough with the components of in _default.page.server.jsx to pursue this further, sorry.

@brillout
Copy link
Member

(Still) a user land thing. As for the last error double check the streaming docs.

@brillout
Copy link
Member

Let me know if you believe there is a problem that is caused by vite-plugin-ssr (so far I'm fairly confident that it's not the case).

@arthurgailes
Copy link
Contributor Author

Thank you again for your help. I've attempted to add an example to the project of how to implement this in practice.

@brillout
Copy link
Member

Neat 💯.

@arthurgailes
Copy link
Contributor Author

I've come to realize my example is actually a poor one. As far as I can tell, there simply is no idiomatic way to load a client-only component within SSR/SSG for vite-plugin-ssr, a la the Next.js dynamic import feature.

My leaflet branch shows this problem with react-leaflet:

function ClientSideComponent() {
  // This code would fix the problem, but produce hydration errors
  // const isBrowser = typeof window !== "undefined";
  // if (!isBrowser) return null;
  const Map = React.lazy(() => import("./Map"));
  return (
    <React.Suspense fallback={<Loading />}>
      <Map />
    </React.Suspense>
  );
}

Rendering this way doesn't make the component client-side only; it makes the component render lazily on the client side and the server side. So it can't help with code that can only be run on the client (leaflet, mapbox, Tableau, etc).

The only option here seems to be to use the "isBrowser" path above and live with the hydration errors, or render the whole page in SPA.

This seems low on the priority list, but maybe make a note of it in the docs?

@brillout
Copy link
Member

Yes the docs should be updated. Thanks for the udpate.

Isn't there a way to tell React to render the Suspense fallback on the server-side?

There must be a way since AFAICT it's what Next.js's dynamic(() => import('./some-component'), { ssr: false }) does.

@arthurgailes
Copy link
Contributor Author

arthurgailes commented Dec 21, 2022

It seems like they feed suspense a conditional component, don't follow well enough to implement, but the gist is:

<Suspense fallback={fallback}>
      <NoSSR>
        <NoSSRComponent {...props} />
      </NoSSR>
    </Suspense>

Their NoSSR function throws an error on the server, but they must have some back-end mechanism for collecting it that I'm not seeing.

@brillout
Copy link
Member

I'm thinking a simpler solution could be to use a wrapper that renders <Loading /> while using useEffects() to dynamically import() the actual component.

@arthurgailes
Copy link
Contributor Author

arthurgailes commented Dec 21, 2022

Oy, I feel like an idiot. That works!

function ClientSideComponentEffects() {
  const [Map, setMap] = React.useState(() => Loading);
  const isBrowser = typeof window !== "undefined";

  React.useEffect(() => {
    if (isBrowser) setMap(() => React.lazy(() => import("./Map")));
  }, [isBrowser]);

  return (
    <React.Suspense fallback={<Loading />}>
      <Map />
    </React.Suspense>
  );
}

@arthurgailes
Copy link
Contributor Author

I've updated my example page to use this example. Thanks for your help.

@brillout
Copy link
Member

FYI you don't need isBrowser as React only runs useEffect() in the browser, and I think Suspense isn't needed at all here?

Thanks for the example, I'll update the VPS docs.

@arthurgailes
Copy link
Contributor Author

Correct on useEffect; updated example.

React Suspense is required to process React.lazy, as the import is async.

@brillout
Copy link
Member

Docs updated https://vite-plugin-ssr.com/dynamic-import.

Thanks for reaching out and apologies for the initial push back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants