实现一个异步按钮组件

场景:

日常开发中按钮 loading 状态一般配合异步函数的调用,这种 button 状态的维护虽然可以,但是如果有很多按钮就需要定义对应的按钮状态,如果是按钮组的话还涉及关联影响的问题;是不是可以实现一个AsyncButton 组件,当传入异步函数的时候自动维护状态呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const ExampleButton = () => {
const [loading, setLoading] = useState(false) // highlight-line

// 异步提交表单数据
const handleSubmit = async (formData) => {
setLoading(true)
try {
// 模拟异步请求
await new Promise((resolve) => setTimeout(resolve, 2000))
// 提交表单数据
// ...
} catch (error) {
// 处理错误
} finally {
setLoading(false)
}
}

return (
<Button
loading={loading} // 添加loading
onClick={async () => {
await handleSubmit()
}}
>
提交
</Button>
)
}

AsyncButton 组件

传入异步函数自动维护 loading 状态

点击按钮 → 执行 onClick → 检测到 Promise → 显示 loading → 执行完异步函数 → 关闭 loading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { FC } from 'react'

interface AsyncButtonProps extends Omit<ButtonProps, 'onClick'> {
onClick: () => Promise<unknown>;
}

const AsyncButton: FC<AsyncButtonProps> = ({
onClick,
children,
...restProps
}) => {
const [innerLoading, setInnerLoading] = useState(false)

const handleClick = () => {
const result = onClick()

if (
result &&
typeof result.then === 'function' &&
typeof result.finally === 'function'
) {
// 同步操作,当异步函数执行时,立即loading;
setInnerLoading(true)
result.finally(() => {
setInnerLoading(false)
})
}
}

return (
<Button loading={innerLoading} onClick={handleClick} {...restProps}>
{children}
</Button>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { AsyncButton } from './components/AsyncButton'

function App() {
const handleClick = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('异步操作完成')
// reject('失败')
resolve('成功')
}, 3000)
})
}
return (
<>
<AsyncButton onClick={() => handleClick()}>点击</AsyncButton>
</>
)
}

export default App

通过检测 Promisethenfinally 方法来自动管理 loading 状态:
promise是链路回调且无论是resolve还是reject,都会走到finally中关闭loading状态,保障loading的正确结束。


添加 Modal 和 Popconfirm 弹框功能,增强 button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { FC } from 'react'

interface AsyncButtonProps extends Omit<ButtonProps, 'onClick'> {
onClick: () => Promise<unknown>;
}

const AsyncButton: FC<AsyncButtonProps> = ({
onClick,
children,
...restProps
}) => {
const [innerLoading, setInnerLoading] = useState(false)

const handleClick = () => {
const result = onClick()

if (
result &&
typeof result.then === 'function' &&
typeof result.finally === 'function'
) {
// 同步操作,当异步函数执行时,立即loading;
setInnerLoading(true)
result.finally(() => {
setInnerLoading(false)
})
}
}

return (
<Button loading={innerLoading} onClick={handleClick} {...restProps}>
{children}
</Button>
)
}

扩展功能:

1、 asyncButton 支持受控模式,此时是一个普通button;
2、 支持Modal弹框功能,点击按钮后弹出Modal;
3、 支持按钮组Button.Group,多个按钮组时,共享状态;

EasyButton 组件设计与目录结构分析

目录结构

组件的目录结构非常规范:

1
2
3
4
5
6
EasyButton/
├── ButtonProvider.tsx # 上下文提供者,管理多按钮状态
├── README.md # 组件文档和使用示例
├── hooks.ts # 自定义 hooks
├── index.tsx # 主组件文件
└── types.ts # TypeScript 类型定义

设计规范分析

1. 文件组织规范

  • 单一职责原则:每个文件都有明确的职责

    • :主组件逻辑
    • :类型定义
    • :可复用的状态逻辑
    • :上下文管理
  • 完整的文档 包含详细的使用示例和 API 文档

2. 组件设计规范

核心特性

  • 自动 Loading 管理:当 onClick 返回 Promise 时自动进入 loading 状态
  • 二次确认交互:支持 modal 和 popup 两种确认方式
  • 权限控制:内置权限管理,支持隐藏或禁用状态
  • 多按钮状态同步:通过 Provider 实现按钮组的状态管理

API 设计规范

1
2
3
4
5
6
7
8
9
10
export type EasyButtonProps = Omit<ButtonProps, 'onClick'> & {
onClick?: () => void | Promise<unknown>
confirm?: React.ReactNode
confirmType?: 'modal' | 'popup'
tooltip?: React.ReactNode
disabledTooltip?: React.ReactNode
group?: string
hasPermission?: boolean
noPermissionStatus?: 'hide' | 'disabled'
}

3. 代码质量规范

TypeScript 使用

  • 完整的类型定义,继承并扩展 Antd Button 的 Props
  • 泛型和联合类型的合理使用
  • 良好的类型推导和约束

Hooks 设计

  • :管理 Popconfirm 和 Tooltip 的状态冲突
  • :统一的 loading 状态管理

状态管理

  • 使用 Context API 实现跨组件状态共享
  • 自定义的安全 useState 防止组件卸载后的状态更新

4. 用户体验规范

交互设计

  • 智能渲染:根据是否需要 tooltip 和 confirm 选择不同的渲染策略
  • 状态冲突处理:Popconfirm 打开时自动隐藏 Tooltip
  • 权限友好:无权限时可选择隐藏或禁用

国际化支持

  • 集成了 useIntl 进行多语言支持
  • 默认文案可配置

5. 扩展性规范

组合模式

1
EasyButton.Provider = ButtonProvider

渐进增强

  • 基础功能:自动 loading
  • 增强功能:二次确认、权限控制、状态同步
  • 保持向后兼容

设计亮点

  1. 渐进式 API:从简单的自动 loading 到复杂的多按钮状态管理
  2. 性能优化:智能渲染,避免不必要的组件嵌套
  3. 开发体验:减少样板代码,自动处理异步状态
  4. 可维护性:清晰的文件结构和职责分离
  5. 类型安全:完整的 TypeScript 支持

这个组件展现了优秀的 React 组件设计模式,在功能完整性、代码质量、用户体验和可维护性方面都达到了很高的标准。