前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8773
  • 传统 API 网关为何无法保护 AI Agent 及解决方案
  • AI Agent 事故响应:滚动消失前应记录的关键数据
  • 字节跳动训练10万亿参数大模型对标Anthropic
  • MasterGo MCP Server 打通设计工具与 IDE:减少上下文切换认知损耗
  • Node.js AI Agent 工具超时:超时机制设计与模型可见性
  • AI助手为何总是答非所问:RAG系统的版本地狱
  • 我在 Claude Code 用了一周十亿 token?97% 是缓存
  • 我把 croniter 移植到 Rust 拿了 228/228,但这个数字什么也证明不了
  • 用代码量化品牌在 AI 搜索引擎中的可见度:GEO 扫分工具
  • 多模态模型的真正瓶颈在最弱编码器
  • Cloudflare 解读 Agent 时代的 Bot 行为评分体系
  • SvelteKit 配置可直接写入 vite.config.js
  • Cloudflare 统一 AI Gateway 与 Workers AI,提供统一路由与计费
  • Cloudflare Radar Researcher:用自然语言探索全球互联网流量数据
  • 在 Claude Code 中使用 Kimi K3 省钱提效
  • Matt Pocock 技能套装安装量突破 54 万次
  • 2026 年 AI 模型格局:主要玩家一览及选型指南
  • 电商从 1 到 1000 单的工程架构演进实战
  • AI Agent 让分布式系统双写问题代价飙升
  • Stack Overflow 流量暴跌 78%,AI 编码助手改变开发问题解决方式
  • Flow Render:用async/await思维重构UI交互
  • 我为何不再信任系统提示词的优先级
  • OpenAI GPT-5.6 发布:Sol/Terra/Luna 三层分级,Free 用户默认使用 Terra
  • 用 TypeScript 构建 Image-to-Video 提示词规划 MCP Server 教程
  • 链上 AI Agent 的信任边界设计实践
  • RAG Agent 行动前的人类在环机制设计
  • Uber 四个月烧完全年 AI 编码预算,微软停用付费编码工具
  • GitHub Actions 和 Pages 降级可用性的应对指南
  • 48个prompt改写实例:信息位置影响模型准确率
  • n8n框架:识别AI流水线中的静默错误
  • 你可能不需要专用向量数据库
  • MemoFS:为TypeScript AI Agent打造文件持久化记忆层
  • Awareness:本地优先的AI Agent记忆工具,R@5达96%
  • 研究实验:AI Agent 能做开放式研究吗?
  • 单 Go 协程跑 8 Gbps 视频流:雷群问题实战
  • Cloudflare CI SDK 发布:TypeScript 替代 YAML 定义流水线
  • AI 能写代码后,程序员的品味才是剩余价值
  • 用 Node.js + Playwright 构建自主式 AI 浏览器 Agent
  • Unsloth 本地微调大模型:VRAM 减半、速度翻倍
  • 本周5条真正落地的AI工作流经验
  • Inngest 对比 Diagrid Catalyst:企业级 Agent 平台选型
  • MCP应用测试与调试:生产级实战指南
  • 五大科技公司联合制定AI Agent插件开放标准
  • 我做了llms.txt但从没有任何爬虫访问过
  • Anthropic Mythos 5 在安全测试中自主发动供应链攻击
  • Agent 账单杀手:被忽视的 Cache-Hit 计费逻辑
  • Token价格周报:NVIDIA Nemotron 3 Super涨价70%
  • 实战:如何为ERP集成构建AI Agent
  • 企业AI Agent生产级五大挑战
  • AI生成代码应在沙箱中运行:免费执行框架
  • Claude Code计划模式:何时真正有用
  • 已加载 51 / 8773
8.0
热点
AI SCORE
编程提效2026-08-07 18:37

Flow Render:用async/await思维重构UI交互

dev.to · AI#React#异步#开源
Editor brief · 编辑速览

Flow Render提出Promise-based UI渲染范式,将交互逻辑转为线性async/await控制流,提升代码可维护性。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

Flow Render 提供了一种基于 Promise 的 UI 渲染方案。它让你像调用异步函数一样渲染组件、await 其结果,并在同一个异步流程中继续执行。

它将分散在状态、回调和组件层级中的交互逻辑整合为线性的 async/await 控制流。业务工作流和 UI 组件各自保持高内聚,而 Promise 结果在二者之间建立了清晰的边界,降低了耦合度,使复杂交互保持专注、可读和可维护。

const confirmed = await render(ConfirmDialog, {
  title: 'Delete your workspace?'
})

if (!confirmed) return

await deleteWorkspace()

当代码确实需要暂停等待用户操作时,使用 Flow Render:

  • 确认一个危险操作
  • 在对话框中完成表单
  • 选择账户、方案或目标
  • 接受权限请求
  • 推进多步骤工作流
  • 从中断或失败的操作中恢复

Flow Render 专注于交互流程。你的页面、本地状态、服务端状态和路由仍然留在它们原本所属的位置。

Flow Render 模型

完整的 React 配置包含两个部分:挂载一个 Viewport,然后从事件处理器中 await 一个组件。

npm install @flow-render/react

将 Viewport 放在应用 providers 内部,靠近组件树的根部。

import { Viewport } from '@flow-render/react'

export function App() {
  return (
    <>
      <AppProviders>
        <Routes />
        <Viewport />
      </AppProviders>
    </>
  )
}

3. 让对话框返回一个值

import type { PromiseResolvers } from '@flow-render/react'

type ConfirmDialogProps = PromiseResolvers<boolean> & {
  title: "string"
  description?: string
}

export function ConfirmDialog({
  title,
  description,
  resolve,
  reject,
}: ConfirmDialogProps) {
  return (
    <dialog open aria-labelledby="confirm-title">
      <h2 id="confirm-title">{title}</h2>
      {description && <p>{description}</p>}

      <footer>
        <button type="button" onClick={() => resolve(false)}>
          Cancel
        </button>
        <button type="button" onClick={() => resolve(true)}>
          Confirm
        </button>
        <button type="button" onClick={() => reject(new Error('Dismissed'))}>
          Dismiss
        </button>
      </footer>
    </dialog>
  )
}
import { render } from '@flow-render/react'

async function handleDelete() {
  const confirmed = await render(ConfirmDialog, {
    title: "'Delete this project?',"
    description: 'This cannot be undone.'
  })

  if (!confirmed) return

  await deleteProject()
}

这就是整体思路。render() 将组件添加到已挂载的 Viewport 中并返回一个 Promise。调用 resolve(value) 用该值结算 Promise;调用 reject(reason) 拒绝它。当 Promise 结算后,Flow Render 会移除该组件。

JavaScript 已经为异步操作赋予了直接、可读的形态:

const response = await fetch('/api/projects')
const project = await response.json()

await animation.finished
await sleep(300)

但 UI 通常表现为断连的状态转换:

setConfirmDialogOpen(true)

这一行没有说明接下来会发生什么。后续逻辑分散在别处:回调函数、effect、prop 链、reducer 分支或状态机转换中。

用户交互同样是异步的。用户看到 UI、做出决定,最终产生多种结果之一。Flow Render 让这种交互使用与现代 JavaScript 其余部分相同的控制流原语:

const answer = await askUser()
const confirmed = await render(ConfirmDialog, { title: 'Ship this release?' })

if (!confirmed) return

await publishRelease()

这并不是说每个组件都应该被 await。持久的侧边栏、内联编辑器、可排序表格或页面级过滤器通常是普通的有状态 UI。有用的边界更窄:

当异步流程的下一步依赖于一个有限的用户交互完成时,await UI。

考虑一个典型的危险操作。快乐路径很短:询问、等待、删除。而状态驱动版本必须将这个路径分散到多个地方。

function ProjectActions({ projectId }: { projectId: string }) {
  const [confirmOpen, setConfirmOpen] = useState(false)
  const [isDeleting, setIsDeleting] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  async function handleConfirm() {
    setIsDeleting(true)
    setError(null)

    try {
      await deleteProject(projectId)
      setConfirmOpen(false)
    } catch (error) {
      setError(error as Error)
    } finally {
      setIsDeleting(false)
    }
  }

  return (
    <>
      <button onClick={() => setConfirmOpen(true)}>Delete project</button>

      <ConfirmDialog
        open={confirmOpen}
        busy={isDeleting}
        error={error}
        onCancel={() => setConfirmOpen(false)}
        onConfirm={handleConfirm}
      />
    </>
  )
}

这段代码本身没有错。React 状态是处理长生命周期、交互式页面的正确工具。当一个事件启动一个短生命周期的交互并需要继续时,摩擦就出现了。原始的故事被拆分到了状态声明、事件处理器、props 和清理逻辑中。

Flow Render 将编排逻辑保持在一起,同时让对话框本身保持为一个普通组件:

async function handleDelete(projectId: string) {
  const confirmed = await render(ConfirmDialog, {
    title: 'Delete project?',
    description: 'All project data will be permanently removed.'
  })

  if (!confirmed) return

  await deleteProject(projectId)
}

随着流程增长,这种差异变得更加明显。一连串的对话框、表单、权限检查和网络请求从上往下阅读,而不是从状态转换到状态转换。

Flow Render 模型

Flow Render 有三个活动部件。

sequenceDiagram
  participant H as Event handler
  participant R as render()
  participant V as Viewport
  participant U as User

  H->>R: render(ConfirmDialog, props)
  R->>V: add ConfirmDialog
  V->>U: show dialog
  U->>V: click Confirm
  V->>R: resolve(true)
  R-->>H: Promise resolves with true
  H->>H: continue flow

该组件仍然是框架组件。它可以使用你的设计系统、hooks、context、CSS、无障碍原语和动画库。Flow Render 只负责其临时挂载和 Promise 边界。

通过 resolve 值而非副作用

一个感知 Promise 的 UI 组件应该通过 resolve 或 reject 表达其结果。

type PlanPickerProps = PromiseResolvers<{ planId: string }> & {
  plans: Array<{ id: string; name: string }>
}

function PlanPicker({ plans, resolve, reject }: PlanPickerProps) {
  return (
    <section aria-label="Choose a plan">
      {plans.map((plan) => (
        <button key={plan.id} onClick={() => resolve({ planId: plan.id })}>
          Choose {plan.name}
        </button>
      ))}
      <button onClick={() => reject(new Error('No plan selected'))}>
        Close
      </button>
    </section>
  )
}

调用方获得一个类型化的值,并拥有接下来的处理权:

const { planId } = await render(PlanPicker, { plans })
await changeSubscription(planId)

这使得临时组件更容易复用:选择器负责选择方案;它不需要知道每个调用方如何更新账单、路由、分析或通知。

两种集成风格

Executor 模式是推荐的默认方式。在组件 props 中声明 resolve 和 reject。Flow Render 在渲染组件时注入它们。

type NameEditorProps = PromiseResolvers<string> & {
  initialValue: string
}

function NameEditor({ initialValue, resolve, reject }: NameEditorProps) {
  const [name, setName] = useState(initialValue)

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        resolve(name.trim())
      }}
    >
      <label>
        Project name
        <input value={name} onChange={(event) => setName(event.target.value)} />
      </label>
      <button type="button" onClick={() => reject(new Error('Editing cancelled'))}>
        Cancel
      </button>
      <button type="submit">Save</button>
    </form>
  )
}

Adapter 模式适用于你无法或不希望修改其回调 props 的已有组件。

type LegacyConfirmProps = {
  open: boolean
  title: string
  onCancel: () => void
  onConfirm: () => void
}

function LegacyConfirmDialog(props: LegacyConfirmProps) {
  return (
    <dialog open={props.open}>
      <p>{props.title}</p>
      <button onClick={props.onCancel}>Cancel</button>
      <button onClick={props.onConfirm}>Confirm</button>
    </dialog>
  )
}

const confirmed = await render<LegacyConfirmProps, boolean>(
  LegacyConfirmDialog,
  (resolve) => ({
    open: true,
    title: 'Archive this project?',
    onCancel: () => resolve(false),
    onConfirm: () => resolve(true)
  })
)

Adapter 模式特意充当一座桥。它让 Flow Render 能与组件库或遗留组件协同工作,同时保留其现有 API。

没有 Promise 边界时,父组件通常需要同时管理可见性状态和最终表单值。

const [editOpen, setEditOpen] = useState(false)
const [pendingProfile, setPendingProfile] = useState<Profile | null>(null)

function handleEdit() {
  setEditOpen(true)
}

function handleProfileSaved(profile: Profile) {
  setEditOpen(false)
  setPendingProfile(profile)
}

useEffect(() => {
  if (!pendingProfile) return

void saveProfile(pendingProfile)
  setPendingProfile(null)
}, [pendingProfile])

使用 Flow Render 后,表单结果就是一个值。

async function handleEdit() {
  const profile = await render(ProfileEditor, { initialProfile: currentProfile })
  await saveProfile(profile)
}

状态驱动的代码通常需要在对话框打开时单独存储目标路径。

const [nextPath, setNextPath] = useState<string | null>(null)

function requestNavigation(path: string) {
  if (!isDirty) {
    navigate(path)
    return
  }

setNextPath(path)
  setLeaveDialogOpen(true)
}

function confirmNavigation() {
  setLeaveDialogOpen(false)
  if (nextPath) navigate(nextPath)
  setNextPath(null)
}

使用 Flow Render 后,目标路径保留在需要它的流程内部。

async function requestNavigation(path: string) {
  if (!isDirty) {
    navigate(path)
    return
  }

const shouldLeave = await render(DiscardChangesDialog)

if (shouldLeave) navigate(path)
}

线性版本使产品决策点清晰可见。

async function inviteMember() {
  const member = await render(MemberFormDialog)

const role = await render(RolePickerDialog, {
    email: member.email
  })

const confirmed = await render(ConfirmDialog, {
    title: `Invite ${member.email} as ${role.name}?`
  })

if (!confirmed) return

await sendInvite({ ...member, roleId: role.id })
}

无需 reducer 状态来记住下一步是什么。每个组件保持独立可测试,并负责一个交互。

本指南使用 React,因为它是最常见的起点。同样的模型也适用于 Vue、Preact、Svelte 和 Solid。

npm install @flow-render/react

或使用其他包管理器:

pnpm add @flow-render/react
yarn add @flow-render/react
bun add @flow-render/react

默认的 render 函数使用该包导出的默认 Viewport。将该 viewport 挂载在树中一个对需要运行的交互始终可用的位置。

import { Viewport } from '@flow-render/react'

export function Root() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <App />
        <Viewport />
      </AuthProvider>
    </ThemeProvider>
  )
}

不要将 viewport 放在对话框所依赖的 provider 之外。Flow Render 渲染的组件出现在 Viewport 下,因此它会接收树中该位置的上下文。

构建一个可等待的组件

将 PromiseResolvers<Value> 添加到 props 中。它的 resolve 回调接受返回给调用者的值;reject 则拒绝待处理的 Promise。

import { useState } from 'react'
import type { PromiseResolvers } from '@flow-render/react'

type QuantityDialogProps = PromiseResolvers<number> & {
  initialQuantity: number
}

export function QuantityDialog({
  initialQuantity,
  resolve,
  reject,
}: QuantityDialogProps) {
  const [quantity, setQuantity] = useState(initialQuantity)

return (
    <dialog open>
      <form
        onSubmit={(event) => {
          event.preventDefault()
          resolve(quantity)
        }}
      >
        <label>
          Quantity
          <input
            type="number"
            min="1"
            value={quantity}
            onChange={(event) => setQuantity(Number(event.target.value))}
          />
        </label>
        <button type="button" onClick={() => reject(new Error('Quantity not selected'))}>
          Cancel
        </button>
        <button type="submit">Continue</button>
      </form>
    </dialog>
  )
}

从事件处理器中等待它

import { render } from '@flow-render/react'

async function handleAddToCart() {
  const quantity = await render(QuantityDialog, {
    initialQuantity: 1
  })

await addToCart({ sku: 'starter-kit', quantity })
}

render() 在允许异步的函数中最自然:按钮处理器、mutation 回调、路由守卫、命令处理器,或从客户端调用的应用服务。

有意识地处理 dismissal

有两个好的约定。为每个组件系列选择一个,并为其团队编写文档。

当取消是一个预期的产品选择时,解析一个中性值。

type ConfirmDialogProps = PromiseResolvers<boolean>

function ConfirmDialog({ resolve }: ConfirmDialogProps) {
  return (
    <dialog open>
      <button onClick={() => resolve(false)}>Cancel</button>
      <button onClick={() => resolve(true)}>Confirm</button>
    </dialog>
  )
}

当调用者必须区分已完成的交互与 dismissal 或中断时,使用 reject。

try {
  const profile = await render(ProfileEditor)
  await saveProfile(profile)
} catch (error) {
  reportDismissal(error)
}

不要仅仅为了建模一个正常的"否"答案而使用 reject。像 false、undefined 或可辨识别的结果这样的值通常会产生更清晰的调用者代码。

下面的示例故意很小。它们展示 Promise 边界应该放在哪里;组件可以使用任何 UI 库或视觉风格。

确认破坏性操作

async function handleRemoveMember(member: Member) {
  const confirmed = await render(ConfirmDialog, {
    title: `Remove ${member.name}?`,
    description: 'They will lose access immediately.'
  })

if (!confirmed) return

await removeMember(member.id)
  toast.success(`${member.name} was removed`)
}

在模态框中创建资源

async function handleCreateProject() {
  const draft = await render(ProjectFormDialog, {
    initialValues: {
      name: '',
      visibility: 'private'
    }
  })

const project = await createProject(draft)
  navigate(`/projects/${project.id}`)
}

表单拥有验证和编辑状态。调用者拥有网络请求和导航,因为这些是流程中的下一步。

继续前要求登录

async function handleExport() {
  if (!session.user) {
    const authenticated = await render(LoginDialog, {
      redirectTo: location.pathname
    })

if (!authenticated) return
  }

await exportReport()
}

LoginDialog 可以解析布尔值、用户对象或认证结果。返回调用者所需的最小值。

async function enableNotifications() {
  const approved = await render(NotificationPermissionDialog)

if (!approved) return

const permission = await Notification.requestPermission()

if (permission === 'granted') {
    await subscribeToNotifications()
  }
}

产品解释是 UI;浏览器权限请求是一个异步平台操作。它们自然组合。

选择支付方式,然后支付

async function checkout(invoice: Invoice) {
  const paymentMethod = await render(PaymentMethodPicker, {
    methods: invoice.availablePaymentMethods
  })

const confirmed = await render(ConfirmPaymentDialog, {
    amount: invoice.total,
    paymentMethod
  })

if (!confirmed) return

await payInvoice({ invoiceId: invoice.id, paymentMethodId: paymentMethod.id })
}

运行多步骤向导

async function createWorkspace() {
  const details = await render(WorkspaceDetailsStep)
  const members = await render(InviteMembersStep, { workspaceName: details.name })
  const plan = await render(PlanPicker)

const workspace = await createWorkspace({
    ...details,
    memberEmails: members.map((member) => member.email),
    planId: plan.id
  })

navigate(`/workspaces/${workspace.id}`)
}

对于紧密耦合的向导,具有共享的后退按钮和持久化草稿状态,一个有状态向导组件可能是更好的选择。当每个交互都有不同的、可重用的结果,并且编排可以在一个函数中可见时,使用单独的等待步骤。

防护未保存的更改

async function requestCloseEditor() {
  if (!editor.isDirty) {
    closeEditor()
    return
  }

const action = await render(UnsavedChangesDialog)

if (action === 'save') {
    await editor.save()
  }

if (action === 'discard' || action === 'save') {
    closeEditor()
  }
}

可辨识别的结果使多个选择变得明确。

type UnsavedAction = 'save' | 'discard' | 'stay'
type UnsavedChangesDialogProps = PromiseResolvers<UnsavedAction>

重试失败的操作

async function publishWithRecovery(post: DraftPost) {
  try {
    await publishPost(post)
  } catch (error) {
    const nextAction = await render(PublishFailedDialog, { error })

```javascript
if (nextAction === 'retry') {
      await publishPost(post)
    }

if (nextAction === 'save-draft') {
      await saveDraft(post)
    }
  }
}

选择上传目标

async function handleUpload(file: File) {
  const folder = await render(FolderPickerDialog, {
    initialFolderId: currentFolder.id
  })

await uploadFile({ file, folderId: folder.id })
}
async function completeOnboarding() {
  const goal = await render(GoalPicker)
  const preferences = await render(NotificationPreferences)

await updateOnboarding({ goal, preferences })
  navigate('/home')
}

代码之所以像产品旅程,是因为它就是产品旅程。

Flow Render 提供三种渲染器作用域。选择与要启动的 UI 匹配的存活周期。

默认渲染器在匹配的 Viewport 挂载后即可使用。

import { render, Viewport } from '@flow-render/react'

export function App() {
  return (
    <>
      <Routes />
      <Viewport />
    </>
  )
}

export async function openGlobalConfirm() {
  return render(ConfirmDialog, { title: 'Continue?' })
}

将其用于应用级别的交互,这类交互应该在特定页面组件卸载后继续存活。

useRenderer() 为组件提供一个绑定到其自身生命周期的渲染器。

import { useRenderer } from '@flow-render/react'

function BillingPanel() {
  const [render, Viewport] = useRenderer()

async function editInvoice() {
    const update = await render(InvoiceEditorDialog)
    await updateInvoice(update)
  }

return (
    <section>
      <button onClick={editInvoice}>Edit invoice</button>
      <Viewport />
    </section>
  )
}

当本地 viewport 卸载时,该渲染器中未完成的任务会被拒绝并抛出取消错误。这防止了临时 UI 在其所有者消失后泄漏。

import { isCancelError, useRenderer } from '@flow-render/react'

function SearchPanel() {
  const [render, Viewport] = useRenderer()

async function chooseFilter() {
    try {
      const filter = await render(FilterPicker)
      applyFilter(filter)
    } catch (error) {
      if (isCancelError(error)) return
      throw error
    }
  }

return <><button onClick={chooseFilter}>Filter</button><Viewport /></>
}

createRenderer() 创建一个独立的渲染函数和 Viewport 对。当库希望暴露一个聚焦的 API 而不希望消费者与应用级渲染器耦合时,这很有用。

import { createRenderer } from '@flow-render/react'

const [renderBillingUi, BillingViewport] = createRenderer()

export function BillingProvider({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <BillingViewport />
    </>
  )
}

export function openUpgradeDialog() {
  return renderBillingUi(UpgradeDialog)
}

这样库的实现细节就被隐藏在它自己的 openUpgradeDialog() 函数背后。

公共 API 有意保持精简。React 包导出了以下原语:

import {
  createRenderer,
  isCancelError,
  render,
  useRenderer,
  Viewport,
  type PromiseResolvers,
  type RenderOptions
} from '@flow-render/react'
render(Component, propsOrAdapter?, options?) => Promise<Value>

将 Component 渲染到关联的 viewport 并返回其结果的 Promise。

const name = await render(NameEditor, {
  initialValue: 'Untitled project'
})

对于一个感知 Promise 的组件,普通对象包含除 resolve 和 reject 之外的所有 props;Flow Render 提供这些回调。

type NameEditorProps = PromiseResolvers<string> & {
  initialValue: string
}

当组件不暴露 resolver props 时,改为传入一个适配器。

const option = await render<ExistingPickerProps, Option>(ExistingPicker, (resolve, reject) => ({
  options,
  onSelect: resolve,
  onClose: () => reject(new Error('Picker closed'))
}))

PromiseResolvers<Value>

interface PromiseResolvers<Value = unknown> {
  readonly resolve: (value: Value) => void
  readonly reject: (reason?: unknown) => void
}

将此类型添加到组件 props 中,以便在调用点推断 Value 的类型。

type ColorPickerProps = PromiseResolvers<{ hex: string }> & {
  initialHex: string
}

async function chooseColor() {
  const color = await render(ColorPicker, { initialHex: '#0ea5e9' })
  // color is { hex: string }
}

对于只确认完成的组件,使用 PromiseResolvers<void>。

type NoticeProps = PromiseResolvers<void> & { message: string }

function Notice({ message, resolve }: NoticeProps) {
  return <button onClick={() => resolve()}>{message}</button>
}

Viewport 是默认渲染器的挂载点。在调用默认渲染函数之前,将其放入框架树中。

import { Viewport } from '@flow-render/react'

function RootLayout() {
  return (
    <Providers>
      <App />
      <Viewport />
    </Providers>
  )
}

它直接渲染待处理的组件。它不强制使用 portal、overlay 或视觉系统。这是刻意设计的:你的对话框组件决定它的外观和行为。

useRenderer() => [render, Viewport]

为当前组件创建一个渲染器对。当临时 UI 应由本地功能拥有时使用它。

const [render, Viewport] = useRenderer()

参见本地渲染器获取完整示例。

createRenderer() => [render, Viewport]

创建一个独立于组件的渲染器对。用于封装子系统或可复用包。

const [renderNotifications, NotificationsViewport] = createRenderer()

renderNotifications 函数只渲染到 NotificationsViewport。

interface RenderOptions {
  exitDelay?: number
}

当组件需要在解决或拒绝后保持挂载一小段时间以便运行退出动画时,传入以毫秒为单位的 exitDelay。

await render(FadeOutDialog, null, { exitDelay: 180 })

Promise 会立即 settled。exitDelay 仅延迟 DOM 移除;它

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
Stack Overflow 流量暴跌 78%,AI 编码助手改变开发问题解决方式
下一篇
我为何不再信任系统提示词的优先级