gatsby: Error with Suspense and lazy loading with gatbsy 4.23.1 and react 18.2 on build

Preliminary Checks

Description

I am unable to use <Suspense> and lazy loading with gatsbyjs 4.23.1 and react 18.2.0. The problem arises with the build and in dev mode with DEV_SSR flag.

The error I get is: 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.

Repo 1 code: https://t.co/VPlZUmZ8d0 Repo 1 preview: https://t.co/Rr7NebYwYA Repo 2 code&preview: https://codesandbox.io/s/suspense-error-n0gby5

Link to a thread with @Paulie about that: https://twitter.com/hellovizart/status/1572961555959975936?s=20&t=Ed9sYfMRBN7TSo-50Wz9Mg

Reproduction Link

https://t.co/Rr7NebYwYA

Steps to Reproduce

  1. gatsby dev

or

  1. gatsby build / gatsby serve

Expected Result

Lazy loading work without error

Actual Result

Error: 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.

Environment

System:
    OS: Windows 10 10.0.19044
    CPU: (4) x64 Intel(R) Core(TM) i5-4670K CPU @ 3.40GHz    
  Binaries:
    Node: 18.7.0 - E:\Program Files\nodejs\node.EXE
    Yarn: 1.22.10 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.4.1 - E:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (105.0.1343.42)
  npmPackages:
    gatsby: ^4.23.0 => 4.23.0
    gatsby-plugin-canonical-urls: ^4.23.0 => 4.23.0
    gatsby-plugin-image: ^2.23.0 => 2.23.0
    gatsby-plugin-manifest: ^4.23.0 => 4.23.0
    gatsby-plugin-no-sourcemaps: ^4.21.0 => 4.23.0
    gatsby-plugin-offline: ^5.23.0 => 5.23.0
    gatsby-plugin-robots-txt: ^1.7.1 => 1.7.1
    gatsby-plugin-sass: ^5.23.0 => 5.23.0
    gatsby-plugin-sharp: ^4.23.0 => 4.23.0
    gatsby-plugin-sitemap: ^5.23.0 => 5.23.0
    gatsby-source-filesystem: ^4.23.0 => 4.23.0
    gatsby-source-shopify: ^6.10.2 => 6.10.2
    gatsby-transformer-json: ^4.23.0 => 4.23.0
    gatsby-transformer-sharp: ^4.23.0 => 4.23.0
  npmGlobalPackages:
    gatsby-cli: 4.23.1

Config Flags

DEV_SSR: true

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 3
  • Comments: 38 (10 by maintainers)

Commits related to this issue

Most upvoted comments

@graysonhicks I see you gave a 👍 to the above suggested solution. Does this mean the Gatsby team recognizes this rather large bug and are working on it?

With DEV_SSR not working with React 18 and Suspense, Gatsby doesn’t actually fully support React 18… which is required for using Gatsby 5.

This seems like a priority bug at the least.

Not sure if the Gatsby team is aware of this but:

  • Gatsby still recommends loadable components here and here
  • Loadable components do not support React 18
  • Gatsby v5 requires React 18

Thanks @lezan I’ve triaged this now!

Try this.

Suspense Helper

I works great in my gatsby 5.12.4 just be sure to add react like below. Tutorial is missing Suspense call in react as `import * as React from ‘react’

import { ReactNode, useEffect, useState, Suspense } from 'react

type Props = { fallback?: ReactNode, children: ReactNode }

export const SuspenseHelper: React.FC<Props> = ({fallback, children}) => {

`const [isMounted, setMounted] = useState<boolean>(false);

useEffect( () => {
    if(!isMounted)
    {
        setMounted(true);
    }
})

return (
    <Suspense fallback={fallback}>
        {!isMounted ? fallback : children}
    </Suspense>
)

};`

then wrap your component as so

const Header = React.lazy(() => import ('@/components/Header'));

<SuspenseHelper fallback={<div>Loading...</div>}> <Header /> </SuspenseHelper>

With this solution I got a Minified React error #421

about the DEV_SSR issue, It seems that the issue is here: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/ssr-develop-static-entry.js#L300 ssr-develop-static-entry.js is using renderToString instead of renderToPipeableStream

I’ve tried to path my local Gatsby code with

diff --git a/node_modules/gatsby/cache-dir/ssr-develop-static-entry.js b/node_modules/gatsby/cache-dir/ssr-develop-static-entry.js
index 3b472ac..136c498 100644
--- a/node_modules/gatsby/cache-dir/ssr-develop-static-entry.js
+++ b/node_modules/gatsby/cache-dir/ssr-develop-static-entry.js
@@ -1,7 +1,7 @@
 /* global BROWSER_ESM_ONLY */
 import React from "react"
 import fs from "fs-extra"
-import { renderToString, renderToStaticMarkup } from "react-dom/server"
+import { renderToString, renderToStaticMarkup, renderToPipeableStream } from "react-dom/server"
 import { get, merge, isObject, flatten, uniqBy, concat } from "lodash"
 import nodePath from "path"
 import { apiRunner, apiRunnerAsync } from "./api-runner-ssr"
@@ -12,6 +12,7 @@ import { RouteAnnouncerProps } from "./route-announcer-props"
 import { ServerLocation, Router, isRedirect } from "@gatsbyjs/reach-router"
 import { headHandlerForSSR } from "./head/head-export-handler-for-ssr"
 import { getStaticQueryResults } from "./loader"
+import { WritableAsPromise } from "./server-utils/writable-as-promise"
 
 // prefer default export if available
 const preferDefault = m => (m && m.default) || m
@@ -297,8 +298,22 @@ export default async function staticPage({
     // If no one stepped up, we'll handle it.
     if (!bodyHtml) {
       try {
-        bodyHtml = renderToString(bodyComponent)
+        //bodyHtml = renderToString(bodyComponent)
+
+        const writableStream = new WritableAsPromise()
+         const { pipe } = renderToPipeableStream(bodyComponent, {
+           onAllReady() {
+             pipe(writableStream)
+           },
+           onError(error) {
+             writableStream.destroy(error)
+           },
+         })
+
+         bodyHtml = await writableStream;
+
       } catch (e) {
+        console.error("SSR Error", e);
         // ignore @reach/router redirect errors
         if (!isRedirect(e)) throw e
       }

but I’m not sure if is correct or there are other places that needs to be updated and it seems that the hot reload is not working correctly.