-
Notifications
You must be signed in to change notification settings - Fork 1
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
How to Show Task Sequence Progress with React Suspense and RSC in Next.js #10
Comments
How to Show Task Sequence Progress with React Suspense and RSC in Next.jsAuthors
Imagine you allow a user to make a purchase of a service or product. If you use one of the popular payment gateways, you can easily show the payment page provided by the payment gateway. Once the user completes the payment, the payment gateway usually does two things:
Now say you want to show the steps that are performed on the confirmation or success page:
All of these steps won't happen instantly. So you want to show the user which step is currently being performed to keep the user informed. Something similar to this: The solutionThere are several ways to achieve this. One way is to use services like Inngest and Trigger.dev. Once you set up the tasks in one of those services, you can trigger the functions and then check the status of the tasks by calling the endpoints provided by the services. Or you can simply create multiple route handlers and call them one after another from the client side. But for every endpoint call, you need to manually maintain the state of the progress and keep the UI in sync using Both of these approaches are very doable, but they have a few drawbacks: they are tedious to code and maintain. RSC and Suspense: The new wayIf you haven't read, I wrote about a new way to fetch data in the era of React Server Components and Suspense. Using the technique described in the article, we can easily implement the confirmation page above. This is how it will look like: As shown in the video, every task is represented by a component that has either a checkmark icon or a spinner icon, a title, and a description which I call the app/confirm/components.tsx type Step = {
title: string;
description: string;
work: Promise<any>;
};
type StepProps = Step & {
isLast: boolean;
};
export function StepComponent({ title, description, work, isLast }: StepProps) {
return (
<li className={`ml-6 ${isLast ? "" : "mb-10"}`}>
<span className="absolute -left-4 flex h-8 w-8 items-center justify-center rounded-full bg-white ring-4 ring-white dark:bg-gray-700 dark:ring-gray-900">
<Suspense fallback={<StepIcon status={"in-progress"} />}>
<Asyncable work={work}>
<StepIcon status={"done"} />
</Asyncable>
</Suspense>
</span>
<Suspense fallback={<Title disabled={true}>{title}</Title>}>
<Asyncable work={work}>
<Title>{title}</Title>
</Asyncable>
</Suspense>
<p className="text-sm">{description}</p>
</li>
);
}
const Asyncable = ({
work,
children,
}: {
work: Promise<any>;
children: React.ReactNode;
}) => {
use(work); // tell React to suspend the component until the promise is resolved which will show the nearest fallback component
return <>{children}</>;
};
const Title = ({
children,
disabled,
}: {
children: React.ReactNode;
disabled?: boolean;
}) => {
return (
<h3
className={cn(
`font-medium leading-tight text-green-500 dark:text-green-400`,
disabled && "text-gray-500",
)}
>
{children}
</h3>
);
}; The In this component, the icon and the color of the title are determined by whether the promise is resolved or not. If the promise is resolved, the icon is a checkmark and the title's color is green. Otherwise, the icon is a spinner and the title's color is gray. In the era before Suspense, we would have to use conditional rendering to determine whether to show the icon or the spinner. But with Suspense, we can keep everything modular. We can think of the spinner as the fallback component while the checkmark is the main component. And since we want to show the fallback when the component is suspended, I created a helper component called <Suspense fallback={<Title disabled={true}>{title}</Title>}>
<Asyncable work={work}>
<Title>{title}</Title>
</Asyncable>
</Suspense> By making things composable like this, we can keep the main component clean, dumb, and only does one thing. In the example above, the Now let's talk about the tasks. First, I made a function that executes several tasks in sequence: export function unsafe_createSequentialProcesses<T extends any[], R>(
...processes: [(arg?: any) => Promise<T[0]>, ...((arg: any) => Promise<any>)[]]
): Promise<R>[] {
return processes.reduce((acc, process, index) => {
if (index === 0) {
return [process(undefined)]
}
return [...acc, acc[acc.length - 1].then(process)]
}, [] as Promise<any>[])
} The I prefixed the function with We can use this function in a Next.js app/confirm/page.tsx export default async function AsyncWorks({
params: { id },
}: {
params: { id: string };
}) {
const [first, second, third] = unsafe_createSequentialProcesses(
() => firstProcess(id),
secondProcess,
thirdProcess,
);
return (
<div className="flex flex-col space-y-4 px-4 py-8">
<VerticalSteps>
<StepComponent
title="This is process 1"
description="It starts immediately when the page is loaded. After it finishes, the UI will automatically update and show the green checkmark."
work={first}
isLast={false}
/>
<StepComponent
title="This is process 2"
description="This process will run after the first one finishes."
work={second}
isLast={false}
/>
<StepComponent
title="This is process 3"
description="While waiting, the component is suspended and shows the loader."
work={third}
isLast={true}
/>
</VerticalSteps>
</div>
);
} The As you can see, a lot of things happen automatically. We just need to make sure the tasks are executed sequentially in the server and the UI is updated automatically. We don't need to manually fetch statuses of the tasks from the client component. In result, we don't need to maintain the states of the progresses and keep the UI in sync using ConclusionI really like this pattern. It doesn't feel tedious. Everything is composed in a modular way. The React components are clean and only do one thing. The only drawback of this approach is that a task can only be in one of the two states: Another thing to note if you deploy your app to Vercel, you may want to increase the serverless function timeout to avoid timeout errors. |
如何在 Next.js 中使用 React Suspense 和 RSC 显示任务序列进度作者
想象一下,您允许用户购买服务或产品。如果您使用流行的支付网关之一,您可以轻松显示该支付网关提供的支付页面。一旦用户完成支付,支付网关通常会做两件事:
现在假设您要显示在确认或成功页面上执行的步骤: 1.确认支付成功。有时,当用户到达确认页面时,您的服务器可能尚未收到来自提供商的 Webhook。因此,用户需要继续执行第一步,直到收到 Webhook 并确认付款。 所有这些步骤都不会立即发生。因此,您希望向用户显示当前正在执行哪个步骤,以便让用户了解情况。与此类似的东西: 解决方案有多种方法可以实现这一目标。一种方法是使用 Inngest 和 Trigger.dev 等服务。在其中一项服务中设置任务后,您可以触发这些功能,然后[检查任务的状态](https://www.inngest.com/docs/examples/fetch-run-status-and-通过调用服务提供的端点来输出#fetching-triggered-function-status-and-output)。 或者您可以简单地创建多个路由处理程序并从客户端依次调用它们。但对于每个端点调用,您需要手动维护进度状态并使用“useState”或“useEffect”保持 UI 同步。 这两种方法都非常可行,但它们有一些缺点:编码和维护都很繁琐。 RSC 和 Suspense:新方式如果你还没有读过,我写了一篇关于 [React Server Components 和 Suspense 时代获取数据的新方法](/blog/simplify-data-fetching-with-rsc-suspense-and-use-api-in -下一个-js)。使用文章中描述的技术,我们可以轻松实现上面的确认页面。 它将如下所示: 如视频所示,每个任务都由一个组件表示,该组件具有复选标记图标或旋转图标、标题和描述(我将其称为“StepComponent”): 应用程序/确认/components.tsx type Step = {
title: string;
description: string;
work: Promise<any>;
};
type StepProps = Step & {
isLast: boolean;
};
export function StepComponent({ title, description, work, isLast }: StepProps) {
return (
<li className={`ml-6 ${isLast ? "" : "mb-10"}`}>
<span className="absolute -left-4 flex h-8 w-8 items-center justify-center rounded-full bg-white ring-4 ring-white dark:bg-gray-700 dark:ring-gray-900">
<Suspense fallback={<StepIcon status={"in-progress"} />}>
<Asyncable work={work}>
<StepIcon status={"done"} />
</Asyncable>
</Suspense>
</span>
<Suspense fallback={<Title disabled={true}>{title}</Title>}>
<Asyncable work={work}>
<Title>{title}</Title>
</Asyncable>
</Suspense>
<p className="text-sm">{description}</p>
</li>
);
}
const Asyncable = ({
work,
children,
}: {
work: Promise<any>;
children: React.ReactNode;
}) => {
use(work); // tell React to suspend the component until the promise is resolved which will show the nearest fallback component
return <>{children}</>;
};
const Title = ({
children,
disabled,
}: {
children: React.ReactNode;
disabled?: boolean;
}) => {
return (
<h3
className={cn(
`font-medium leading-tight text-green-500 dark:text-green-400`,
disabled && "text-gray-500",
)}
>
{children}
</h3>
);
};
在此组件中,标题的图标和颜色由 Promise 是否已解决决定。如果承诺得到解决,则图标为复选标记,标题颜色为绿色。否则,图标是一个旋转图标,标题的颜色是灰色的。 在 Suspense 之前的时代,我们必须使用条件渲染来确定是否显示图标或旋转器。但通过 Suspense,我们可以保持一切模块化。我们可以将旋转器视为后备组件,而复选标记是主要组件。由于我们希望在组件挂起时显示回退,因此我创建了一个名为“Asyncable”的辅助组件,用于挂起组件直到承诺得到解决: <Suspense fallback={<Title disabled={true}>{title}</Title>}>
<Asyncable work={work}>
<Title>{title}</Title>
</Asyncable>
</Suspense> 通过使事物像这样可组合,我们可以保持主要组件干净、愚蠢,并且只做一件事。在上面的例子中,“Title”组件只负责根据“disabled”属性渲染标题和标题的颜色。它没有暂停组件的责任。 “Asyncable”组件可以做到这一点。如果后备组件可以是完全不同的组件,则“Title”组件甚至不需要有条件地渲染不同的颜色。 现在我们来谈谈任务。首先,我创建了一个按顺序执行多个任务的函数: export function unsafe_createSequentialProcesses<T extends any[], R>(
...processes: [(arg?: any) => Promise<T[0]>, ...((arg: any) => Promise<any>)[]]
): Promise<R>[] {
return processes.reduce((acc, process, index) => {
if (index === 0) {
return [process(undefined)]
}
return [...acc, acc[acc.length - 1].then(process)]
}, [] as Promise<any>[])
}
我在函数前面加上了“unsafe_”前缀,因为它不是强类型的。我尝试将其设为强类型,但我的 TypeScript 技能还不足以做到这一点。即使是法学硕士也无法帮助我🫠。如果您知道如何强类型化此函数,请告诉我! 我们可以在 Next.js 应用程序/确认/page.tsx export default async function AsyncWorks({
params: { id },
}: {
params: { id: string };
}) {
const [first, second, third] = unsafe_createSequentialProcesses(
() => firstProcess(id),
secondProcess,
thirdProcess,
);
return (
<div className="flex flex-col space-y-4 px-4 py-8">
<VerticalSteps>
<StepComponent
title="This is process 1"
description="It starts immediately when the page is loaded. After it finishes, the UI will automatically update and show the green checkmark."
work={first}
isLast={false}
/>
<StepComponent
title="This is process 2"
description="This process will run after the first one finishes."
work={second}
isLast={false}
/>
<StepComponent
title="This is process 3"
description="While waiting, the component is suspended and shows the loader."
work={third}
isLast={true}
/>
</VerticalSteps>
</div>
);
} “firstProcess”、“secondProcess”和“thirdProcess”是异步函数。我们将这些承诺传递给每个“StepComponent”组件。 正如您所看到的,很多事情都是自动发生的。我们只需要确保任务在服务器中按顺序执行,并且 UI 会自动更新。我们不需要从客户端组件手动获取任务的状态。因此,我们不需要维护进度状态并使用“useState”或“useEffect”保持 UI 同步。 结论我真的很喜欢这个图案。并不觉得乏味。一切都是以模块化的方式组成的。 React 组件很干净,只做一件事。 这种方法的唯一缺点是任务只能处于两种状态之一:“待处理”或“完成”。它没有“等待”状态。这是由于 React 的 Suspense 的本质是基于 Promise 的本质。我有一个解决这个问题的想法,但那是另一天的想法。 另一件需要注意的事情是,如果您将应用程序部署到 Vercel,您可能需要[增加无服务器函数超时](https://vercel.com/changelog/serverless-functions-can-now-run-up-to-5-分钟)以避免超时错误。 |
export function unsafe_createSequentialProcesses<T extends any[], R>(
...processes: [(arg?: any) => Promise<T[0]>, ...((arg: any) => Promise<any>)[]]
): Promise<R>[] {
return processes.reduce((acc, process, index) => {
if (index === 0) {
return [process(undefined)]
}
return [...acc, acc[acc.length - 1].then(process)]
}, [] as Promise<any>[])
} =.- 因为是递归执行的。所以确定了第一个的类型后面的类型就跟着确定了。I mean 返回值 |
客户端使用 如果是服务端就直接 但是值得注意的是. 需要一个队列服务去 hold 主对应的任务. 也就是没成功的时候要一直卡主 |
The original blog info
The text was updated successfully, but these errors were encountered: