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

Suspense and Server-Side Rendering #1906

Open
pkellner opened this issue Apr 6, 2022 · 35 comments
Open

Suspense and Server-Side Rendering #1906

pkellner opened this issue Apr 6, 2022 · 35 comments
Labels
area: suspense bug Something isn't working

Comments

@pkellner
Copy link

pkellner commented Apr 6, 2022

Bug report

Description / Observed Behavior

React 18 and SWR fails on the simplest of examples. I've created a sandbox at this URL: https://codesandbox.io/s/long-cdn-qxkc3v?file=/pages/indexSuspense.js that if your browse (to that page), you'll get the error:

error: The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.

Expected Behavior

I expect to see a page rendered just like at the default root which is in the file /pages/index.js

Repro Steps / Code Example

Run the code sandbox, browse to http://.../indexSuspense and you will get the error I observe

Additional Context

Below is the code from code sandbox along with the associated package.json

import { Suspense } from "react";
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function Profile() {
  const { data } = useSWR("/api/cities", fetcher, {
    suspense: true
  });
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

export default function Example() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Profile />
    </Suspense>
  );
}

package.json

{
  "name": "example-react-suspense",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "start": "next start"
  },
  "dependencies": {
    "next": "12.1.4",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "swr": "^1.2.2"
  }
}

I've also been testing with the latest canary from next and the latest pre-release from react 18.1 and getting the same errors.

erro1

@shuding shuding added bug Something isn't working area: suspense labels Apr 6, 2022
@shuding
Copy link
Member

shuding commented Apr 11, 2022

@pkellner I just checked your example, and it turns out that since React 18 now supports Suspense on the server side, that fetch('/api/cities') will happen on the server. So I saw this error:

TypeError: Only absolute URLs are supported
at getNodeRequestOptions ...

which is the root cause. You can't fetch a relative URL (/api/cities) on the server side.

In fact, if you change it to an absolute API like https://api.github.com/repos/vercel/swr it will SSR correctly.

@shuding
Copy link
Member

shuding commented Apr 11, 2022

I will keep this issue open until we add more clarification and guidance on Suspense and SSR to the documentation. Let me know if there's any suggestion on this!

@pkellner
Copy link
Author

pkellner commented Apr 11, 2022

Thanks @shuding for the help here. I'm not sure whether I should continue this issue, as it's related to instability, or start another one, as the problem has morphed but it I think it's still SWR and Suspense instability, as this example I'm about to describe works correctly when using with create-react-app (please let me know and I'll be happy to re-create this as a new issue).

Here is the new problem. I've slightly modified the example shown above to include a simple loop that renders a button with a click event.

https://github.com/pkellner/swr_issue_1906_suspense and associated codesandbox

Actual Behavior

With the component included (that has the suspense boundary and useSwr call), when clicking on any of the three buttons the first time, the click event does not get fired. However, on the 2nd or 3rd click it does fire.

With the component commented out (removed), the button click event fires and you see the console.log message immediately on the first time clicking any row.

Expected Behavior

I expect that when clicking on a button the first time, it will output the console message in the click event whether or not a Suspense element is included in the render.

Aside Note

I've included a slightly more complex source code example here that I'm tracking with an issue on the React repository that has nothing to do with this issue, but points out the complexity of using Suspense compared to not using Suspense. I did the example using create-react-app and not nextjs because of this issue (which just makes the example over there confusing to run). That is, I've brought that example back to this repo running in NextJS. You can browse to that code running with the absorbing click event here by browsing to the URL: http://localhost:3000/indexApp

Associated Relevant Code

import { Suspense } from "react";
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function Profile() {
  //const url = "/api/cities";
  const url = "https://airquality.peterkellner.net/api/city";
  const { data } = useSWR(url, fetcher, {
    suspense: true,
  });
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

/*
If comment out call to <Profile /> the console.log on the click event happens correctly.  That is, on the first click.
  If you do not comment out <Profile /> as is shown here, the first time you cick on any of the three rows, the click is not executed until
  the second or third time you click a button.
 */

export default function Example() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {[1, 2, 3].map((counter) => {
        return (
          <button key={counter}
            onClick={() => {
              console.log(`click Me clicked ${counter}`);
            }}
          >
            Click Me {counter}
          </button>
        );
      })}
      <Profile />
    </Suspense>
  );
}

@gaearon
Copy link

gaearon commented Apr 11, 2022

It seems a bit confusing to me that useSWR runs on the server at all, given that it's not designed to transfer data to the client. So there is no way for it to hydrate properly. Would it be better to disable useSWR on the server altogether and make it throw until there is some strategy?

@devknoll
Copy link

devknoll commented Apr 11, 2022

@gaearon yeah, that was one concern I had here.

My feeling is that most suspense-like libraries probably don't properly support SSR, given as how until React 18 it was not possible to run them on the server at all. It might be nice if e.g. updating these libraries to throw during SSR was a more official recommendation?

@shuding
Copy link
Member

shuding commented Apr 12, 2022

Yep @gaearon @devknoll this is what we are going to do, there is also #1832 and #1841.

Our plan is to:

  • 1.x which is the current version: show a warning if you are using SWR and suspense: true on the server side.
  • 2.x which will come in a couple of weeks: change the warning above to an error as its breaking.

The tricky thing is, frameworks do pre-rendering to improve SEO, etc. SWR doesn’t want to run on the server, but unlike fetching data inside an effect, there’s no easy way to skip that with Suspense on the server side, without causing an error.

@shuding
Copy link
Member

shuding commented Apr 12, 2022

most suspense-like libraries probably don't properly support SSR

I see this as a general problem of “Suspense for data fetching in SSR” though.

But still, if the data is not going to cause a mismatch (e.g. fully static, or “almost” static as hydration happens very soon after SSR, usually), Suspense in SSR still makes sense.

@gaearon
Copy link

gaearon commented Apr 12, 2022

My understanding from a convo with @sebmarkbage is that a proper solution for useSWR is to take “initial data” argument. That’s taken from getServerSideProps on the server. Which means it also becomes available on the client for hydration. The initial fetch happens by calling the actual implementation (not /api/ route) from the server itself. So there’s also no “absolute URL” problem because there’s no fetch call on the server at all. Then useSWR on the client hydrates with that initial data. And for future navigations on the client useSWR does actual requests.

Then you only need to throw if you’re on the server and data wasn’t preloaded.

@stefee
Copy link

stefee commented Apr 12, 2022

piotr-cz added a commit to piotr-cz/swr-idb-cache that referenced this issue Apr 12, 2022
@pkellner
Copy link
Author

@shuding , do you have any insight now into a path forward to supporting Suspense with SWR? Above you mentioned I believe, that you plan on disabling the suspense: true option shortly. Is that still your thinking?

I'm also wondering if whether using CRA vs NextJS has any impact on this. I'm assuming that Suspense integration with CRA is just as broken on CRA as it is on Next but I'm not 100% clear on that. If your implementation does work correctly for CRA with Suspense enabled, I'd love to know that for sure. I understand from @gaearon that CRA integration might not work either.

Thanks for paying attention to this. I can't say much more publicly, but I have managed to delay my course work on React 18 with Suspense until this is sorted.

@gaearon
Copy link

gaearon commented Apr 14, 2022

Suspense integration with CRA is just as broken on CRA as it is on Next

To be clear, there is nothing broken in Suspense integration with Next. What's broken here is the useSWR helper and how it implements Suspense. Suspense is only ready for usage in data frameworks (like Relay), not in ad-hoc helpers like useSWR. So the useSWR story is not fleshed out, and that's where the issue is from.

The issue is specific to server rendering, so it won't appear in CRA. However, still, we don't recommend using Suspense implementations in ad-hoc data libraries like useSWR in stable code at the moment. Suspense-powered opinionated frameworks are OK.

@pkellner
Copy link
Author

pkellner commented Apr 14, 2022

I know asking timeframe type questions is fraught with peril, but does this feel like something that will get sorted in weeks, months, or many months? (afraid to say years).

Also, besides the FakeAPI example which is clearly meant NOT for production, are the any cases where Suspense could currently be used in production (that is not using Relay specifically).

@gaearon
Copy link

gaearon commented Apr 14, 2022

It seems like https://swr.vercel.app/docs/with-nextjs#pre-rendering-with-default-data gives you a solution that should work.

@pkellner
Copy link
Author

pkellner commented Apr 18, 2022

I realize I posted this on the wrong issue so I've reposted it here:

facebook/react#23045 (comment)

@mashaal
Copy link

mashaal commented May 14, 2022

It seems a bit confusing to me that useSWR runs on the server at all, given that it's not designed to transfer data to the client. So there is no way for it to hydrate properly. Would it be better to disable useSWR on the server altogether and make it throw until there is some strategy?

@shuding - Wasn't a potential strategy the extend cache provider feature?

We've been using this without issue in our deno/react framework, we populate the cache server side, which is used to hydrate client side. The standard SWR options can be configured to prevent fetch calls on mount, if this is the desired outcome.

It seems most of the comments are in the 'it works, but not exactly how we want' way, instead of throwing an error, couldn't this be something that is opt-in and documented a bit more clearly?

@pkellner
Copy link
Author

Has there been any progress on getting SWR to work reliably with Suspense?

@pkellner
Copy link
Author

@gaearon , in another issue on the Apollo repo, another user is claiming that Suspense with react-query is stable in production if using just client side rendering. Could you clarify if that is true please.

apollographql/apollo-client#9627 (comment)

@gaearon
Copy link

gaearon commented May 23, 2022

The only Suspense implementation we've vetted so far is Relay. There is ongoing work on the Next.js side to add support for Suspense Data Fetching, but it will be through React Server Components and might not support refetching in the first iteration (but later will). Any broader support is not official at this moment, and you can expect additions/changes in the low-level APIs as we figure this out. So while today's implementations might "work", we can't vouch for them, and there might be some API and/or implementation changes necessary in them in the future.

@pkellner
Copy link
Author

Hi @shuding , I notice you are working on caching in NextJS with the latest canary. Will this work go towards useSwr working with Suspense correctly? https://github.com/vercel/next.js/pull/37258/files/7401927ee29a53f3203da3b8fd8d2e750a86ea8f

@pkellner
Copy link
Author

pkellner commented Jun 18, 2022

Hi @shuding and @gaearon
In the latest React blog post and the 18.2 release, I don't see any mention of work around Suspense and Data access regarding non relay libraries. Did I miss something? Can you provide an update on status, in particular SWR and Suspense? I remember there was talk about removing the Suspense option from useSWR because of the confusion it creates (that is, it is not recommended for production according to Dan and the React team docs).

Thanks

https://reactjs.org/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.html

@shuding
Copy link
Member

shuding commented Jul 4, 2022

Hey @pkellner! From the SWR side there isn't much I can do – in #1931 we already made SWR behaving correctly with Suspense and SSR. I guess the next step is to have React release something similar to this for libraries to adopt, but I'm unsure.

@shuding shuding changed the title Simple example with React 18 and Suspense fails with Error Suspense and Server-Side Rendering Jul 4, 2022
@fgnass
Copy link

fgnass commented Jul 13, 2022

Hi @gaearon, another use case for data-fetching with suspense on the server, are zero-KB JavaScript environments like Astro or Capri. Unlike Next.js, Capri does not have its own router and can't fetch the data upfront, outside of React. I guess what I'd really want for SSG would be an async version of renderToStaticMarkup that lets components fetch their own data. Until now, using something like SWR with suspense was the only way to achieve this. Can you think of a future-proof way to keep on supporting this pattern?

@pkellner
Copy link
Author

related reactjs/rfcs#229

@rickiesmooth
Copy link

I must be doing something wrong, but when I provide (static) fallback data, my component never suspends and show my fallback loader

@pkellner
Copy link
Author

pkellner commented May 7, 2023

@rickiesmooth can you provide a simple codesandbox type example?

@rickiesmooth
Copy link

rickiesmooth commented May 8, 2023

@pkellner sure thing! I made a simple codesandbox example here: https://codesandbox.io/p/sandbox/suspicious-chaum-u68vrm?file=%2Fapp%2Fpage.tsx&selection=%5B%7B%22endColumn%22%3A22%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A22%2C%22startLineNumber%22%3A12%7D%5D

Notice you never see "Loading..."

I guess it's because by providing the fallbackData there's no pending promise initially, but also providing an unresolved promise as fallback data doesn't trigger the suspense fallback.

@pkellner
Copy link
Author

pkellner commented May 8, 2023

Hi @rickiesmooth Thanks for the example. You are trying to use Suspense on the client side for data and unfortunately, that scenario is not supported by React at the moment. The only real support for suspense and data isn with NextJS 13.4 with server components. You can see a big warning at this URL: https://swr.vercel.app/docs/suspense

@dbk91
Copy link

dbk91 commented May 8, 2023

@rickiesmooth I encountered the same scenario when my team began using the beta App router. The beta App router docs suggested using SWR as an alternative to React's use hook for fetching client-side data. It didn't explicitly call out suspense as a supported option, but it didn't have verbiage that advised against its usage—that was only listed on SWR's docs as @pkellner pointed out.

The interesting thing was that if you simply don't provide the fallbackData, suspense appears to work as expected aside from throwing the error about the missing fallbackData. If I remember correctly, the production app would still work since the error was simply logged to the console and not visible to the user. I'm not sure if this is still the case.

I'm unfamiliar with the internals of React with respect to client-side data fetching and suspense, so I'm sure I'm unaware of pitfalls, bugs, and general misusage, but it would be nice to have some sort of confirmation that this truly would cause bugs in production. Otherwise it could be better to let developers try ignoring the fallbackData error when suspense is turned on. Even if it was as simple as...

const swrConfig = {
  suspense: true,
  unstable_noFallbackDataError: true,
}

@rickiesmooth
Copy link

@dbk91 thanks for sharing your experience!

The interesting thing was that if you simply don't provide the fallbackData, suspense appears to work as expected aside from throwing the error about the missing fallbackData. If I remember correctly, the production app would still work since the error was simply logged to the console and not visible to the user. I'm not sure if this is still the case.

I think this kinda worked, but it would switch to client side rendering? However, when I now build my small codesandbox example it throws the following error:

[    ] info  - Generating static pages (0/3)Error: Fallback data is required when using suspense in SSR.

so it doesn't appear to still be the case.

@dbk91
Copy link

dbk91 commented May 8, 2023

@rickiesmooth Oh right, did forget one caveat. Any component using suspense + SWR would need to opt out of build-time optimization. Potentially a big downside, but the app directory for Next makes this a bit easier to control if the component are organized correctly. I think if you use useSearchParams wherever you're using SWR + suspense, and more importantly before the useSWR hook, it'll opt out of SSG and the build will work. However, that was for versions < 13.4. I'd have to verify that's still the case.

@rickiesmooth
Copy link

@dbk91 thanks for explaining, I'll look into your suggestions and see how far I get.

@shuding could you maybe give some clarification and guidance here?

@rickiesmooth
Copy link

I ended up just opting out of SSR for components that use swr:

export default dynamic(() => Promise.resolve(DashboardLayout), {
  ssr: false,
})

@pkellner
Copy link
Author

pkellner commented Jul 15, 2023

Hi @shuding and @gaearon

I'm really hoping for an update from the React team on supporting adhoc data retrieval with Suspense support in client components. I totally get that this is a hard, and maybe an intractable problem. It would be really helpful to know that this scenario is likely not going to be supported in the future so we can close issues like this and move on.

#1906 (comment)

@dbk91
Copy link

dbk91 commented Jul 22, 2023

Based on the comments in the issue, I've realized there might be two separate issues being discussed here.

@shuding and @gaearon provided solutions for fetching data server-side to hydrate the client, then have SWR handle subsequent requests on the client. This solution works today and is achieved by fetching data server-side and injecting that into fallbackData. However, this leads to @rickiesmooth's observation which is that you won't receive an instant loading state like you would using the combination of a page.js and loading.js in Next.js since the client is already hydrated. So if the goal is to have declarative loading states while retaining traditional SPA-behavior, this won't be the solution.

To achieve the SPA effect using suspense with Next + SWR, you just have to invoke useSearchParams prior to the useSWR hook as I mentioned before. This is because useSearchParams will opt client components out of static-rendering on the server. I have a working solution that pulls my Github profile and displays the loading state. While this works in the production build, you'll unfortunately still see the error with next dev. Originally I thought this might be an issue with SWR and is also the flag I was referring to in my original comment, but I realize this is likely an issue with Next and not SWR. It's unclear to me why Next still opts client components into static-rendering during dev and not build, but there's probably good reasons for it. I'll either file an issue with Next or at least begin a discussion so there is awareness around this.

@pkellner Let me know if this doesn't address your original intent with this issue—I don't want to assume you're trying to achieve traditional SPA behavior.

@pkellner
Copy link
Author

@dbk91 , I'm confident there is only one issue here I've been tracking. I read your other discussion item and I think the point you are missing is that the this code: const { data } = useSWR("dbk91", getProfile, { suspense: true }); is unstable. It was created a very long time ago and really should not be in the library as an option. It is what @gaearon refers to as "adhoc helpers" here in this issue and points out they are unstable. Nothing has changed in that regard. They are still unstable. As you point out, they do work, but unstable to me means not ready for production.

My point in this issue is still "When will their be an implementation of Suspense for adhoc data on the client side that is stable". I see that there is work being done to solve this on the Tanstack that is currently marked as experimental. I'm still not understanding where the React team stands on this and whether they will even get involved. It seems to me that it should be solved at the React team core level and not arbitrary libraries that "we" should not have to depend on for async data support with Suspense.

Here are some links to that experimental work:

https://tanstack.com/query/v4/docs/react/guides/suspense
https://tanstack.com/query/v4/docs/react/community/suspensive-react-query
https://tanstack.com/query/v4/docs/react/guides/suspense

clean-teach added a commit to clean-teach/study-nomadcoders-carrot-market that referenced this issue Feb 2, 2024
## 목표

- 이번 색션에서는 React 18버전에서 새롭게 지원되는 기능들에 대해서 알아본다.
    - NextJS 에서도 이러한 React의 기능들을 지원한다.
    - 프레임워크만 다룰 줄 알고 라이브러리는 다룰 줄 모르면 안되기 때문에 둘 다 알아본다.
- 이번 강의에서는 이미 안정되어 사용가능한 기능인 **Suspense** 에 대하여 알아본다.
    - 주의 :
        - 이 페이지는 **다소 오래되었으며** 역사적인 목적으로만 존재합니다.
        - React 18은 동시성을 지원하면서 출시되었습니다. 그러나 **더 이상 "모드"가 없으며** 새로운 동작은 완전히 선택되어 있으며 [새 기능을 사용할 때만](https://reactjs.org/blog/2022/03/29/react-v18.html#gradually-adopting-concurrent-features) 활성화됩니다 .
    - **Suspense** 기능을 실습해 보기 위해서 캐럿마켓 프로젝트의 `pages/profile/index.tsx` 파일에서 기존에 `getServerSideProps` 로 구현 되었던 부분을 주석처리 하고 유저 정보를 로딩 할 수 있는 환경을 만들어서 실습 하여 본다.

## **Suspense 란?**

- 코드에서 로딩 상태를 나타내는 부분을 제거 할 수 있게 해주는 API
    - 코드에서 로딩 상태에 대해 신경쓰지 않아도 유저가 로딩 화면을 볼 수 있다.
- **Suspense** 는 다음 것들과 함께 사용 할 수 없다.
    - `getServerSideProps`
    - `getStaticProps`
    - `getStaticPaths`

    → 위 기능들을 사용하면, 클라이언트 단에서 따로 로딩을 하지 않기 때문..

## 정리

- **Suspense** 의 장점 중 하나는
    - **Suspense** 로 감싼 컴포넌트에서 받아오는 API 데이터는 이미 로딩이 성공했다는 가정 하에 사용 할 수 있다.

        → 렌더링 코드 부분에서 API 데이터 유효성 확인 및 제외처리를 할 필요가 없다.

- 페이지에서 SWR을 사용하면, **Suspense** 는 SWR 로딩이 끝날때까지 페이지 컴포넌트 전체를 표시하지 않는다.
    - 따라서 로딩 데이터 부분만 로딩 UI을 표시하고 싶다면, 해당 부분만을 컴포넌트로 추출하여 **Suspense** 로 감싸준다.
- **Suspense** 를 활성화 시키는 방법은 라이브러리마다 다르다.
    - **Suspense** 는 개발자가 활성화 시킨다고 되는 것이 아니라, 라이브러리에서 해당 기능을 지원해줘야 하기 때문
    - 예 : SWR와 React Query 의 **Suspense** 를 활성화 시키는 방법은 각각 다르다.

## 참고

### Suspense

[Suspense for Data Fetching (Experimental) – React](https://17.reactjs.org/docs/concurrent-mode-suspense.html)

- Suspense를 사용하면 컴포넌트가 렌더링되기 전까지 기다릴 수 있습니다.
- React 16.6 버전에서는 코드를 불러오는 동안 “기다릴 수 있고”, 기다리는 동안 로딩 상태를 지정할 수 있도록 `< Suspense >` 컴포넌트가 추가되었습니다.
- Suspense는 단순히 데이터 로딩뿐만 아니라 이미지, 스크립트, 비동기 작업을 기다리는 데에도 사용될 수 있습니다.

```tsx
// ProfilePage를 불러오는 동안 Loading를 표시합니다
<Suspense fallback={< Loading / >}>
	<ProfilePage />
</Suspense>
```

### 완전 새로운 리액트가 온다? 핵심정리 10분컷.

https://www.youtube.com/watch?v=7mkQi0TlJQo

### Error: Fallback data is required when using suspense in SSR

> swr의 공식문서(https://swr.vercel.app/ko/docs/suspense)에 따르면, swr의 suspense모드는 next js에서 페이지를 서버사이드 렌더링(이 경우에는 pre-render)할 때 문제가 발생하는 것 같습니다.
>
- 해결법은 크게 두가지
    - 하나는 swrconfig에 fallback 데이터를 미리 제공하는 것

        → 하지만, 이 방법을 사용하면 suspense를 사용하는 의미가 없어집니다. 왜냐하면, suspense의 목적은 클라이언트단에서 실제 데이터를 받은 이후에만 렌더링하는 건데, fallback은 서버사이드에서 데이터를 패치하여 넣어줘야 하기 때문입니다.

        - (서버사이드에서 데이터를 받으면, 클라이언트사이드에서 데이터는 이미 존재하니 suspense의 필요성이 없어짐.)
    - 다른 방법은 next/dynamic을 사용해서 아래처럼 서버사이드 렌더링을 꺼주는 겁니다.
        - `export default dynamic(async () => Page, { ssr: false });`
- 참고 문서 : vercel/swr#1906
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: suspense bug Something isn't working
Projects
None yet
Development

No branches or pull requests

9 participants