Open innocces opened 2 months ago
Authors
Name
Nico Prananta
Data fetching in the React ecosystem has been a hot topic for a long time. Since React is not opinionated about how data is fetched, the community has come up with various solutions.
One solution that is simple and doesn't need any dependencies is using JavaScript's fetch
and the useEffect
hook.
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
However, this approach has some drawbacks. First, without careful handling, it can lead to race conditions, as I have discussed before, and memory leaks, which happen when the component is unmounted before the fetch is completed.
Second, the fetch-in-effect approach causes network waterfall. This means that the data is fetched after the code for the component is downloaded and loaded. Now, if you have a component that fetches data in effect and it renders a child component that fetches data in effect, it will take a while for the child component to be rendered to the user.
Third, you need to write the API endpoints to return the data that your component needs. This can be a bit tedious and error-prone.
The lack of data fetching recommendations from the React team led to several popular solutions like TanStack Query and SWR. They simplify data fetching by providing a set of APIs that handle fetching and caching for you.
Then React finally released Suspense. With Suspense, you can show a fallback component while the actual component is loading. Once the data is fetched, the Suspense component will render the component with the data. Unlike fetch-in-effect, Suspense is designed so that data fetching and code downloading are done in parallel, avoiding the network waterfall problem.
However, it’s important to remember that Suspense is not a data fetching mechanism. If you look at the official documentation of Suspense, you'll notice that React doesn't specifically mention data fetching. It says that Suspense is a mechanism to show a fallback UI while the component is loading. In the examples, they don't even show how the data is fetched. They don't show how you can actually suspend the component to activate Suspense. They recommend using data fetching with Suspense-enabled frameworks like Relay and Next.js.
Non-framework tools like SWR have finally supported Suspense, but it's still experimental and React doesn't actually recommend it. According to the announcement:
Suspense works best when it’s deeply integrated into your application’s architecture: your router, your data layer, and your server rendering environment.
I honestly think this is a mistake by the React team. They created Suspense and know exactly how it works. React should have come with an official API for Suspense-enabled data fetching instead of leaving it to the community to figure out.
Sebastian Markbåge from the Next.js team, who is in the React core team, tweeted recently: "React never released official Suspense support on the client because it leads to client waterfalls. Instead, we shifted to an RSC strategy." I guess making Suspense work on the client without waterfalls is a hard problem to solve. While they stated that they might expose additional primitives that could make it easier to access your data without the use of opinionated frameworks, as of this writing, they focus instead on React Server Components (RSC).
RSC is a somewhat controversial technology. It was created to improve the efficiency of rendering React components on the server. Before RSC, React could already render components on the server, but it wasn't possible to feed the components with data fetched from the server. Frameworks like Next.js use a certain loader function like getServerSideProps
, which allows us to fetch data from a database or third-party API and pass it to the page component as props in server-side rendering.
With RSC, we don't need a dedicated loader function anymore. Every server component can fetch its own data within the component itself. This avoids the need to fetch all data in one place and pass it from the root component to all the child components.
It's controversial because you cannot use RSC by itself. You need to use a framework like Next.js or Waku. And since Next.js is the poster child for RSC, many people accuse Vercel of forcing RSC to make more profit. The complicated caching mechanism and the many ways to render a page don't help the case.
Not to mention that there are many easy-to-misinterpret terms. For example, although they are named server components, they are not only executed and rendered on the server whenever the page is requested. They are also executed and rendered during build time. However, they are also not ALWAYS executed and rendered on the server when a request comes.
By default, server components in Next.js are static, which means they are only executed and rendered during build time. They won't be executed and rendered again on the server when the page is requested. You have to explicitly tell Next.js that your server component is dynamic by exporting a dynamic
constant with the force-dynamic
value, or by calling one of the dynamic functions.
Another confusing term is the use server
directive. To mark a React component as a client component, you can add the use client
directive at the top of the component. Naturally, people will assume that the use server
directive is the opposite of use client
, but it’s not. use server
is a directive that marks server-side functions that can be called from client-side code. It's actually related to Server Actions.
Despite the pitfalls mentioned above, RSC is fun to use in my experience. I've been using it in my work and personal projects for a while now. With RSC and Suspense, we can immediately send the static parts of the page to the client, and the components that need data can be suspended and rendered once the data is ready.
For example, in the following code, when the user visits the /suspense
page, they will immediately see the static parts of the page like the <h1>
and the Footer
.
app/suspense/page.tsx
import { Suspense } from 'react'
import Albums from '../albums'
import Songs from '../songs'
import { headers } from 'next/headers'
import Loading from '../loading'
import Footer from '../footer'
import ErrorBoundaryWithFallback from '../fallback-error'
export default function Page() {
headers()
return (
<div>
<h1 className="text-xl font-bold">Suspense Demo</h1>
<div className="grid h-[400px] w-[400px] grid-cols-2 gap-4 overflow-scroll bg-gray-100 p-4">
<ErrorBoundaryWithFallback>
<Suspense fallback={<Loading />}>
<Albums />
<Songs />
</Suspense>
</ErrorBoundaryWithFallback>
</div>
<Footer />
</div>
)
}
Meanwhile, the albums data is fetched on the server and sent to the client once it's available. While waiting for the albums data, the Loading
component will be rendered. Once the data is fetched, the Albums
component will be rendered with the data.
albums.tsx
import { Suspense } from 'react'
import { albumsData, relatedAlbums } from './albums-data'
export default async function Albums() {
const albums = await albumsData()
return (
<div>
<h1 className="text-xl font-bold">Albums</h1>
{albums.map((album) => (
<div key={album.id}>
<h2>{album.name}</h2>
<p>{album.artist}</p>
<p>{album.year}</p>
<Suspense fallback={<div>Loading related...</div>}>
<RelatedAlbums albumId={album.id} />
</Suspense>
</div>
))}
</div>
)
}
You can see the demo here.
In the example above, the Albums
component wrapped in Suspense
is a server component. But what if the component that needs data fetched on the server is a client component? A client component cannot be an async function. This is where the experimental use
API comes to the rescue. Side note: for some reason, React doesn't call it the use
hook. Instead, it's called the use
API. I wonder why.
With RSC, Suspense, and the use
API, data fetching can start early on the server and be awaited on the client.
app/suspense/hoisted-client/race/page.tsx
import { headers } from 'next/headers'
import Albums from './albums'
import Songs from './songs'
import { albumsData } from '../../albums-data'
import { songsData } from '../../songs-data'
import Footer from '../../footer'
export default function Page() {
headers()
const getAlbumsData = albumsData() // returns a promise but we don't need to await it
const getSongsData = songsData() // returns a promise but we don't need to await it
return (
<div className="p-4">
<h1 className="text-xl font-bold">Suspense Hoisted Race Demo</h1>
<div className="grid h-[400px] w-[400px] grid-cols-2 gap-4 overflow-scroll bg-gray-100 p-4">
<Albums dataSource={getAlbumsData} />
<Songs dataSource={getSongsData} />
</div>
<Footer />
</div>
)
}
albums.tsx
'use client'
import { Suspense, use } from 'react'
import { albumsData } from '../../albums-data'
import ErrorBoundaryWithFallback from '../../fallback-error'
export default function Albums({ dataSource }: { dataSource: ReturnType<typeof albumsData> }) {
return (
<div>
<h1 className="text-xl font-bold">Albums</h1>
<ErrorBoundaryWithFallback>
<Suspense fallback={<div>Loading albums...</div>}>
<AlbumsList dataSource={dataSource} />
</Suspense>
</ErrorBoundaryWithFallback>
</div>
)
}
function AlbumsList({ dataSource }: { dataSource: ReturnType<typeof albumsData> }) {
const albums = use(dataSource) // this causes the AlbumsList to be suspended until the data is available
return (
<>
{albums.map((album) => (
<div key={album.id}>
<h2>{album.name}</h2>
<p>{album.artist}</p>
<p>{album.year}</p>
</div>
))}
</>
)
}
You can check the demo here. In the demo, the data fetching is intentionally delayed and randomly throw errors.
There are several things I like about this pattern aside from how it avoids the network waterfall. First, it's easy to test the component. During the test, we can just pass a function to AlbumsList
that returns a promise with the data needed by AlbumsList
. We don't need an additional library to mock or stub the function.
Second, the component is simplified, which makes it easier to understand. We don't need to implement data fetching with fetch-in-effect or use TanStack Query or SWR. The component doesn't need to know how to implement the fetching logic. It just needs to wait for the data to be available, thanks to the use
API.
Third, Suspense helps to reduce the complexity of the component even more. There's no need to implement conditional rendering for the loading state. It's all handled by Suspense. The component only needs to render the data when it's available.
I'm not saying that you have to use RSC, Suspense, and the use
API. You can still use fetch-in-effect or TanStack Query or SWR. It's all optional. That's one of the great things about React. They take backward compatibility seriously. If you don't like the new API, you can still use the old API. And if you don't like the old API, you can start using the new API. If you want to create a Single Page Application (SPA), you can. The choice is yours.
PS: You might be interested in reading about caching mechanism in RSC here.
Update: I wrote another post showcasing an example of using this pattern. Read How to Show Task Sequence Progress with React Suspense and RSC in Next.js post.
By the way, I have a book about Pull Requests Best Practices. Check it out!
作者
姓名
尼科·普拉南塔
React 生态系统中的数据获取长期以来一直是一个热门话题。由于 React 对于如何获取数据没有固执己见,因此社区提出了各种解决方案。
一种简单且不需要任何依赖项的解决方案是使用 JavaScript 的“fetch”和“useEffect”挂钩。
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
然而,这种方法有一些缺点。首先,如果不仔细处理,它可能会导致竞争条件,正如我之前讨论过的,以及内存泄漏,当组件在组件之前被卸载时会发生这种情况。抓取完成。
**其次,有效获取方法会导致网络瀑布。**这意味着在下载并加载组件代码后才获取数据。现在,如果您有一个有效获取数据的组件,并且它渲染了一个有效获取数据的子组件,则需要一段时间才能将子组件呈现给用户。
第三,您需要编写 API 端点来返回组件所需的数据。这可能有点乏味并且容易出错。
由于 React 团队缺乏数据获取建议,因此出现了几种流行的解决方案,例如 TanStack Query 和 SWR。它们通过提供一组为您处理获取和缓存的 API 来简化数据获取。
然后React终于发布了Suspense。使用 Suspense,您可以在加载实际组件时显示后备组件。获取数据后,Suspense 组件将使用数据渲染组件。 与 fetch-in-effect 不同,Suspense 的设计使得数据获取和代码下载并行完成,避免了网络瀑布问题。
然而,重要的是要记住 Suspense 不是一种数据获取机制。如果你查看官方的Suspense文档,你会发现React并没有特别提到数据获取。它说 Suspense 是一种在组件加载时显示后备 UI 的机制。在示例中,它们甚至没有显示如何获取数据。 **他们没有展示如何实际挂起组件来激活 Suspense。**他们建议使用支持 Suspense 的框架(如 Relay 和 Next.js)来获取数据。
像 SWR 这样的非框架工具终于支持了 Suspense,但它仍然处于实验阶段,而且 [React 实际上并不推荐它](https://react.dev/blog/ 2022/03/29/react-v18#suspense-in-data-frameworks)。根据公告:
当 Suspense 深入集成到您的应用程序架构中时,效果最佳:您的路由器、数据层和服务器渲染环境。
老实说,我认为这是 React 团队的一个错误。他们创造了悬念并确切地知道它是如何运作的。 React 应该附带一个官方 API,用于支持 Suspense 的数据获取,而不是让社区来解决。
来自 Next.js 团队的 Sebastian Markbåge(React 核心团队成员)最近 tweet:“React 从未在客户端上发布官方 Suspense 支持,因为它相反,我们转向了 RSC 策略。” 我想让 Suspense 在没有瀑布的情况下在客户端上工作是一个很难解决的问题。虽然他们表示[他们可能会公开额外的原语,这些原语可以使您在不使用固定框架的情况下更轻松地访问数据](https://react.dev/blog/2022/03/29/react-v18#suspense-in -data-frameworks),在撰写本文时,他们专注于 React 服务器组件(RSC)。
RSC 是一项颇具争议的技术。它的创建是为了提高在服务器上渲染 React 组件的效率。在 RSC 之前,React 已经可以在服务器上渲染组件,但无法向组件提供从服务器获取的数据。像 Next.js 这样的框架使用特定的加载器函数,例如“getServerSideProps”,它允许我们从数据库或第三方 API 获取数据,并将其作为服务器端渲染中的 props 传递给页面组件。
有了 RSC,我们不再需要专用的加载器函数。 **每个服务器组件都可以在组件本身内获取自己的数据。**这避免了需要在一个地方获取所有数据并将其从根组件传递到所有子组件。
这是有争议的,因为您不能单独使用 RSC。您需要使用 Next.js 或 Waku 等框架。由于 Next.js 是 RSC 的典型代表,许多人指责 Vercel 迫使 RSC 赚取更多利润。复杂的缓存机制和多种[渲染页面](https: //nextjs.org/docs/app/building-your-application/rendering)对这种情况没有帮助。
更不用说还有很多容易误解的术语。例如,尽管它们被命名为服务器组件,但它们不仅在请求页面时在服务器上执行和呈现。 它们也在构建期间执行和渲染。然而,当请求到来时,它们也不会总是在服务器上执行和呈现。
默认情况下,Next.js 中的服务器组件是静态,这意味着它们仅在构建期间执行和呈现。当请求页面时,它们不会在服务器上再次执行和呈现。您必须通过[使用“force-dynamic”值导出“dynamic”常量](https://nextjs.org/docs/app/api-reference/)明确告诉 Next.js 您的服务器组件是“动态”的file-conventions/route-segment-config#dynamic),或通过调用[动态函数]之一(https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-功能)。
另一个令人困惑的术语是“use server”指令。要将 React 组件标记为客户端组件,您可以在组件顶部添加 use client
指令。自然地,人们会认为“use server”指令与“use client”相反,但事实并非如此。 [use server
是一个指令,标记可以从客户端代码调用的服务器端函数](https://react.dev/reference/rsc/use-server#noun-labs-1201738-(2 ))。它实际上与服务器操作有关。
尽管存在上述缺陷,但根据我的经验,RSC 使用起来很有趣。我已经在我的工作和个人项目中使用它有一段时间了。借助 RSC 和 Suspense,[我们可以立即将页面的静态部分发送给客户端,一旦数据准备好,就可以暂停并渲染需要数据的组件](https://nextjs.org/docs/app/构建您的应用程序/数据获取/模式#streaming)。
例如,在下面的代码中,当用户访问“/suspense”页面时,他们将立即看到页面的静态部分,例如“h1"和“Footer”。
应用程序/悬念/page.tsx
import { Suspense } from 'react'
import Albums from '../albums'
import Songs from '../songs'
import { headers } from 'next/headers'
import Loading from '../loading'
import Footer from '../footer'
import ErrorBoundaryWithFallback from '../fallback-error'
export default function Page() {
headers()
return (
<div>
<h1 className="text-xl font-bold">Suspense Demo</h1>
<div className="grid h-[400px] w-[400px] grid-cols-2 gap-4 overflow-scroll bg-gray-100 p-4">
<ErrorBoundaryWithFallback>
<Suspense fallback={<Loading />}>
<Albums />
<Songs />
</Suspense>
</ErrorBoundaryWithFallback>
</div>
<Footer />
</div>
)
}
同时,专辑数据会在服务器上获取并在可用后发送到客户端。在等待专辑数据时,将渲染“Loading”组件。获取数据后,“Albums”组件将随数据一起呈现。
专辑.tsx
import { Suspense } from 'react'
import { albumsData, relatedAlbums } from './albums-data'
export default async function Albums() {
const albums = await albumsData()
return (
<div>
<h1 className="text-xl font-bold">Albums</h1>
{albums.map((album) => (
<div key={album.id}>
<h2>{album.name}</h2>
<p>{album.artist}</p>
<p>{album.year}</p>
<Suspense fallback={<div>Loading related...</div>}>
<RelatedAlbums albumId={album.id} />
</Suspense>
</div>
))}
</div>
)
}
您可以在此处查看演示。
在上面的示例中,包裹在“Suspense”中的“Albums”组件是一个服务器组件。 但是,如果需要在服务器上获取数据的组件是客户端组件怎么办? 客户端组件不能是异步函数。这就是实验性的“use” API 发挥作用的地方。旁注:出于某种原因,React 没有将其称为“use”钩子。相反,它被称为“use”API。我想知道为什么。
借助 RSC、Suspense 和“use” API,数据获取可以在服务器上尽早开始并在客户端等待。
应用程序/悬念/hoisted-client/race/page.tsx
import { headers } from 'next/headers'
import Albums from './albums'
import Songs from './songs'
import { albumsData } from '../../albums-data'
import { songsData } from '../../songs-data'
import Footer from '../../footer'
export default function Page() {
headers()
const getAlbumsData = albumsData() // returns a promise but we don't need to await it
const getSongsData = songsData() // returns a promise but we don't need to await it
return (
<div className="p-4">
<h1 className="text-xl font-bold">Suspense Hoisted Race Demo</h1>
<div className="grid h-[400px] w-[400px] grid-cols-2 gap-4 overflow-scroll bg-gray-100 p-4">
<Albums dataSource={getAlbumsData} />
<Songs dataSource={getSongsData} />
</div>
<Footer />
</div>
)
}
专辑.tsx
'use client'
import { Suspense, use } from 'react'
import { albumsData } from '../../albums-data'
import ErrorBoundaryWithFallback from '../../fallback-error'
export default function Albums({ dataSource }: { dataSource: ReturnType<typeof albumsData> }) {
return (
<div>
<h1 className="text-xl font-bold">Albums</h1>
<ErrorBoundaryWithFallback>
<Suspense fallback={<div>Loading albums...</div>}>
<AlbumsList dataSource={dataSource} />
</Suspense>
</ErrorBoundaryWithFallback>
</div>
)
}
function AlbumsList({ dataSource }: { dataSource: ReturnType<typeof albumsData> }) {
const albums = use(dataSource) // this causes the AlbumsList to be suspended until the data is available
return (
<>
{albums.map((album) => (
<div key={album.id}>
<h2>{album.name}</h2>
<p>{album.artist}</p>
<p>{album.year}</p>
</div>
))}
</>
)
}
您可以在此处查看演示。在演示中,数据获取被故意延迟并随机抛出错误。
除了它如何避免网络瀑布之外,我还喜欢这种模式的几个优点。首先,**测试组件很容易。**在测试期间,我们只需将一个函数传递给 AlbumsList
,该函数返回一个包含 AlbumsList
所需数据的 Promise。我们不需要额外的库来模拟或存根该函数。
其次,组件被简化,更容易理解。我们不需要使用 fetch-in-effect 来实现数据获取,也不需要使用 TanStack Query 或 SWR。组件不需要知道如何实现获取逻辑。 它只需要等待数据可用,这要归功于“use” API。
第三,Suspense 有助于进一步降低组件的复杂性。 **加载状态不需要实现条件渲染。**这一切都由 Suspense 处理。该组件仅需要在数据可用时呈现数据。
我并不是说您必须使用 RSC、Suspense 和 use
API。 您仍然可以使用 fetch-in-effect 或 TanStack Query 或 SWR。这都是可选的。这是 React 的伟大之处之一。他们非常重视向后兼容性。如果您不喜欢新的 API,您仍然可以使用旧的 API。如果您不喜欢旧的 API,您可以开始使用新的 API。如果您想创建单页应用程序 (SPA),您可以。 选择是你的。
PS:您可能有兴趣阅读RSC 中的缓存机制。
更新:我写了另一篇文章,展示了使用此模式的示例。阅读如何在 Next.js 中使用 React Suspense 和 RSC 显示任务序列进度 帖子。
顺便说一句,我有一本关于 Pull 请求最佳实践的书。看看!
客户端 use
更讨喜些
The original blog info