From f5108bc53d0be1ef63bd5d0be5970da64ab2d113 Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Thu, 7 Mar 2024 14:31:33 +0100 Subject: [PATCH] Fix SSR error if Suspense is missing --- apps/demo/src/app/loading.tsx | 5 +++++ apps/dev/package.json | 2 +- apps/dev/src/app/loading.tsx | 5 +++++ docs/docs/01-getting-started/01-setup.md | 24 +++++++++++++++-------- packages/stack-server/src/utils/react.tsx | 2 -- packages/stack-shared/src/utils/react.tsx | 11 +++++++---- packages/stack/src/lib/stack-app.ts | 18 ++++++++--------- pnpm-lock.yaml | 21 +++++++++++++++----- 8 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 apps/demo/src/app/loading.tsx create mode 100644 apps/dev/src/app/loading.tsx diff --git a/apps/demo/src/app/loading.tsx b/apps/demo/src/app/loading.tsx new file mode 100644 index 000000000..b0d574600 --- /dev/null +++ b/apps/demo/src/app/loading.tsx @@ -0,0 +1,5 @@ +export default function Loading() { + return <> + Loading... + ; +} diff --git a/apps/dev/package.json b/apps/dev/package.json index a0f6dd3b2..55b82c3e2 100644 --- a/apps/dev/package.json +++ b/apps/dev/package.json @@ -12,7 +12,7 @@ "lint": "next lint" }, "dependencies": { - "next": "14.0.4", + "next": "^14.1", "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", diff --git a/apps/dev/src/app/loading.tsx b/apps/dev/src/app/loading.tsx new file mode 100644 index 000000000..b0d574600 --- /dev/null +++ b/apps/dev/src/app/loading.tsx @@ -0,0 +1,5 @@ +export default function Loading() { + return <> + Loading... + ; +} diff --git a/docs/docs/01-getting-started/01-setup.md b/docs/docs/01-getting-started/01-setup.md index 88ae06505..b0dafe656 100644 --- a/docs/docs/01-getting-started/01-setup.md +++ b/docs/docs/01-getting-started/01-setup.md @@ -60,7 +60,7 @@ npm install @stackframe/stack 4. In your `app/layout.tsx`, wrap your entire layout with a `StackProvider`. Afterwards, it should look like this: ```tsx - import React, { Suspense } from "react"; + import React from "react"; import { StackProvider } from "@stackframe/stack"; import { stackApp } from "@/lib/stack"; @@ -68,11 +68,9 @@ npm install @stackframe/stack return ( - - - {children} - - + + {children} + ); @@ -81,9 +79,19 @@ npm install @stackframe/stack This lets you use the `useStackApp()` and `useUser()` hooks. - Note that Stack uses the new react suspense feature, which abstracted away all the hurdle of handling loading states. Check our [here](https://react.dev/reference/react/Suspense) if you want to learn more about suspense. -5. That's it! Stack is now configured in your Next.js project. If you start your Next.js app with `npm run dev` and navigate to `http://localhost:3000/handler/signup`, you will see the Stack sign-up page! +5. By default, Stack uses [`Suspense`](https://react.dev/reference/react/Suspense) to handle loading states. To show a loading indicator while Stack is fetching user data, make sure there is a `loading.tsx` file in your `app` directory: + + ```tsx + export default function Loading() { + // You can use any loading indicator here + return <> + Loading... + ; + } + ``` + +6. That's it! Stack is now configured in your Next.js project. If you start your Next.js app with `npm run dev` and navigate to `http://localhost:3000/handler/signup`, you will see the Stack sign-up page! ![Stack sign up page](../imgs/signup-page.png) diff --git a/packages/stack-server/src/utils/react.tsx b/packages/stack-server/src/utils/react.tsx index 3c603791d..f4a0cf256 100644 --- a/packages/stack-server/src/utils/react.tsx +++ b/packages/stack-server/src/utils/react.tsx @@ -19,8 +19,6 @@ export function getNodeText(node: React.ReactNode): string { /** * Suspends the currently rendered component indefinitely. Will not unsuspend unless the component rerenders. - * - * You can use this to translate older query- or AsyncResult-based code to new the Suspense system, for example: `if (query.isLoading) suspend();` */ export function suspend(): never { use(neverResolve()); diff --git a/packages/stack-shared/src/utils/react.tsx b/packages/stack-shared/src/utils/react.tsx index d0904f956..141f1b2f4 100644 --- a/packages/stack-shared/src/utils/react.tsx +++ b/packages/stack-shared/src/utils/react.tsx @@ -30,20 +30,23 @@ export function suspend(): never { /** - * Use this in a component or a hook to disable SSR. + * Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error. */ -export function suspendIfSsr() { +export function suspendIfSsr(caller?: string) { if (typeof window === "undefined") { const error = Object.assign( new Error(deindent` - This code path is not supported in SSR. This error should be caught by the closest Suspense boundary, and hence never be thrown on the client. If you still see the error, make sure the component is rendered inside a Suspense boundary. + ${caller ?? "This code path"} attempted to display a loading indicator during SSR by falling back to the nearest Suspense boundary. If you see this error, it means no Suspense boundary was found, and no loading indicator could be displayed. Make sure you are not catching this error with try-catch, and that the component is rendered inside a Suspense boundary, for example by adding a \`loading.tsx\` file in your app directory. - See: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content + See: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout + + More information on SSR and Suspense boundaries: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content `), { // set the digest so nextjs doesn't log the error // https://github.com/vercel/next.js/blob/d01d6d9c35a8c2725b3d74c1402ab76d4779a6cf/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts#L14 digest: "BAILOUT_TO_CLIENT_SIDE_RENDERING", + reason: caller ?? "suspendIfSsr()", } ); diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 2a5c5c621..81ca054c9 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -189,9 +189,9 @@ function getTokenStore(tokenStoreOptions: TokenStoreOptions) { } const loadingSentinel = Symbol("stackAppCacheLoadingSentinel"); -function useCache(cache: AsyncCache, dependencies: D): T { +function useCache(cache: AsyncCache, dependencies: D, caller: string): T { // we explicitly don't want to run this hook in SSR - suspendIfSsr(); + suspendIfSsr(caller); const subscribe = useCallback((cb: () => void) => { const { unsubscribe } = cache.onChange(dependencies, () => cb()); @@ -476,7 +476,7 @@ class _StackClientAppImpl void) { @@ -592,7 +592,7 @@ class _StackClientAppImpl this._projectAdminFromJson( j, this._createAdminInterface(j.id, tokenStore), @@ -806,7 +806,7 @@ class _StackServerAppImpl this._serverUserFromJson(j)); } @@ -936,7 +936,7 @@ class _StackAdminAppImpl this._refreshProject() ); @@ -958,7 +958,7 @@ class _StackAdminAppImpl this._createApiKeySetFromJson(j)); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e01780da..1b1078f3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,11 +118,11 @@ importers: specifier: workspace:* version: link:../../packages/stack-shared next: - specifier: 14.0.4 - version: 14.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.1 + version: 14.1.0(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18 version: 18.2.0 @@ -2719,7 +2719,7 @@ packages: peerDependencies: react: '*' dependencies: - '@types/react': 18.2.60 + '@types/react': 18.2.63 prop-types: 15.8.1 react: 18.2.0 @@ -6006,7 +6006,6 @@ packages: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 csstype: 3.1.3 - dev: false /@types/retry@0.12.0: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -12036,6 +12035,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /next-themes@0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + dependencies: + next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /next@14.0.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==} engines: {node: '>=18.17.0'}