-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Comments
@pkellner I just checked your example, and it turns out that since React 18 now supports Suspense on the server side, that
which is the root cause. You can't fetch a relative URL ( In fact, if you change it to an absolute API like |
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! |
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 BehaviorWith 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 BehaviorI 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 NoteI'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 Codeimport { 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>
);
} |
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? |
@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? |
Yep @gaearon @devknoll this is what we are going to do, there is also #1832 and #1841. Our plan is to:
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. |
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. |
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. |
@gaearon See “Fallback Data” https://swr.vercel.app/blog/swr-v1#fallback-data “Pre-Rendering with Default Data” https://swr.vercel.app/docs/with-nextjs#pre-rendering-with-default-data |
@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. |
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. |
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). |
It seems like https://swr.vercel.app/docs/with-nextjs#pre-rendering-with-default-data gives you a solution that should work. |
I realize I posted this on the wrong issue so I've reposted it here: |
@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? |
Has there been any progress on getting SWR to work reliably with Suspense? |
@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. |
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. |
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 |
Hi @shuding and @gaearon Thanks https://reactjs.org/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.html |
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? |
related reactjs/rfcs#229 |
I must be doing something wrong, but when I provide (static) fallback data, my component never suspends and show my fallback loader |
@rickiesmooth can you provide a simple codesandbox type example? |
@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 |
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 |
@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 The interesting thing was that if you simply don't provide the 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 const swrConfig = {
suspense: true,
unstable_noFallbackDataError: true,
} |
@dbk91 thanks for sharing your experience!
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. |
@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 |
I ended up just opting out of SSR for components that use swr: export default dynamic(() => Promise.resolve(DashboardLayout), {
ssr: false,
}) |
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. |
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 To achieve the SPA effect using suspense with Next + SWR, you just have to invoke @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. |
@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: 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 |
## 목표 - 이번 색션에서는 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
We wanted to support partially rendering parts of the DOM that require client-side requests (fueled by swr) while statically rendering the rest using Next.JS. The secret sauce is the hidden and undocumented "Postpone" method buried within Next.JS's dynamic rendering code which seems to be intended for partial pre-rendering functionality which is tangentially related to what we're trying to do. Next.JS's Postpone seems to use react@experimental's unstable_postpone under the cover. It bails out of server-side rendering and lets the client take over at runtime when called, rendering the nearest Suspense fallback (i.e. loading spinner in our case) until the client takes over. Seems to be working as expected thus far but very untested: "use client";
import { IS_SERVER, SWRConfiguration, SWRResponse } from "swr/_internal";
import useSWR from "swr";
import { usePathname } from "next/navigation";
import { Postpone } from "next/dist/server/app-render/dynamic-rendering";
type SuspenseSWRHook = <TData extends any, TError extends any>(
url?: string,
config?: SWRConfiguration<TData, TError>,
) => SWRResponse<TData>;
const useSuspenseSWR: SuspenseSWRHook = <TData extends any, TError extends any>(
url?: string,
config?: SWRConfiguration<TData>,
) => {
const fallbackData = config?.fallbackData;
// we can't use suspense in SWR when pre-rendering on the server without
// providing fallback data
// as a workaround, when we're on the server w/ no fallback data we bail out
// to client side rendering for the DOM tree that is currently rendering (up to the nearest
// Suspense boundary)
// So basically the below will throw when on the server. When the client takes over it'll continue
// forth and render the DOM tree
if (IS_SERVER && !fallbackData) {
const route = usePathname();
Postpone({
// this info is just used for stack logs in dev mode
// that explain why a bail out on pre rendering occurred
reason: "useSuspenseSWR",
route,
});
}
config = { ...config, suspense: true };
const swrResponse = useSWR<TData, TError>(url, config);
return swrResponse;
};
export default useSuspenseSWR; Note: bare in mind // package.json
{
"next": "15.0.0-canary.134",
"react": "19.0.0-rc-7771d3a7-20240827",
"react-dom": "19.0.0-rc-7771d3a7-20240827",
} I feel like SWR would be improved if it built upon something similar, allowing consumers of the library to use SWR in a way which will bail out of server side rendering when suspense is true and there is no fallback data supplied - yielding the nearest Suspense boundary - until CSR time, where the client takes over. |
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
package.json
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.
The text was updated successfully, but these errors were encountered: