从开发体验探讨React模态窗口实现
varHarrie opened this issue · 0 comments
业务场景
最近的项目出现大量模态窗口的应用场景,大多数业务流程也非常相似:
- 用户点击某个按钮,弹出模态窗口,并传入一些数据
- 这些数据可能直接用于窗口内部展示,也可能用于异步请求获取详情
- 窗口内部有一些表单,详情的数据作为表单的初始值
- 用户点击取消,直接关闭模态窗口
- 用户点击确定,对表单数据进行校验或处理,发送请求保存数据,最后关闭模态窗口
- 为了复用,这个窗口可以同时用于新增和编辑
- 同一个页面类似功能的窗口可能同时存在多个
举个实际的例子,在一个用户列表的页面,用户列表上方有个新建用户按钮
,用户列表的每一项后面都有个编辑用户按钮
。
- 点击
新建用户按钮
,弹出用户模态窗口,里面表单初始值都是空的 - 点击
编辑用户按钮
,弹出用户模态窗口,里面表单初始值从后端获取
实现和改进
按照以往做法,结合一些组件库,把每个业务场景的模态窗口封装成一个组件,例如用户窗口UserModal
,然后在页面组件中引用和渲染,传入visible
、onConfirm
等等。
class UserListView extends React.Component {
state = {
modalVisible: boolean
modalUserId: ''
}
// 打开新建窗口
onCreateModalOpen = () => {
this.setState({modalVisible: true, modalUserId: ''})
}
// 打开编辑窗口
onEditModal = (id) => {
this.setState({modalVisible: true, modalUserId: id})
}
// 窗口确定
onModalConfirm = (data) => {
// 根据data有没有id,来判断调用新建还是保存的接口
// ...
// 根据接口返回信息,若成功,关闭窗口,若失败,弹出提示,保留窗口
// ...
}
// 窗口取消
onModalCancel = () => {
this.setState({ modalVisible: false })
}
render () {
const { modalVisible, modalUserId } = this.state
const users = [/** 从后端获取 */]
<div>
<header>
<button onClick={this.onCreateModalOpen}>新建用户</button>
</header>
<ul>
{users.map((user) => (
<li key={user.id}>
<span>{user.name}</span>
<button onClick={() => this.onEditModal(user.id)}>编辑用户</button>
</li>
))}
</ul>
<UserModal
visible={modalVisible}
id={modalUserId}
onConfirm={this.onModalConfirm}
onCancel={this.onModalCancel}
/>
</div>
}
}
页面组件UserListView
需要维护模态窗口的显示状态visible
,还要维护各种事件onConfirm
、onCancel
。这还是仅仅只考虑模态窗口的逻辑的情况,而且该模态窗口依赖的modalUserId
也放在的页面组件中,某种程度上说,这不是父组件需要的状态。
假如这时候需求变动原因,需要在编辑用户按钮
后面再加一个编辑用户角色
的按钮,点击之后弹出用户角色模态窗口
,用于展示和勾选用户角色。这时,我们又要维护一份新的模态窗口状态、模态窗口事件,包括对应的命名也都要重新考虑。
经过一番思考,如果我们把模态窗口的状态、事件放进这个模块窗口组件中,会怎么样?
将窗口相关的事件、状态,都交给窗口组件自己去维护,只接受onConfirm
事件,再暴露show
、hide
之类的方法,供父组件调用。
class UserListView extends React.Component {
refModal = React.createRef()
// 打开新建或编辑窗口
onModalOpen = (id) => {
this.refModal.current.show(id)
}
// 窗口确定
onModalConfirm = (data) => {
// 返回true,阻止窗口关闭
}
render () {
const users = [/** 从后端获取 */]
<div>
<header>
<button onClick={() => this.onModalOpen()}>新建用户</button>
</header>
<ul>
{users.map((user) => (
<li key={user.id}>
<span>{user.name}</span>
<button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
</li>
))}
</ul>
<UserModal
ref={this.refModal}
onConfirm={this.onModalConfirm}
/>
</div>
}
}
这个时候,页面组件UserListView
对于UserModal
就只需要关注打开窗口并传参(输入),窗口确定事件(输出)。
如果还有其他窗口,也只需要多维护一份ref
、onModalOpen
、onModalConfirm
。
进一步改进
后来,在使用antd的modal函数调用时,联想到这个问题,发现后可以有进一步改进的实现方案。
从开发体验上讲,以上的实现方案,还有一些需要改进的地方:
- 对于
onModalOpen
、onModalConfirm
的维护依然觉得繁琐。每个窗口都需要维护两个函数,从逻辑上讲,它们应该是“连续”的,先有onModalOpen
触发窗口展示,用户操作完后触发onModalConfirm
,获得反馈信息;但是代码上却分隔开来了,不同窗口间的事件还可以混合在一起。 ref
创建之后,还需要通过在组件上传入ref={this.refModal}
,已将它们绑定在一起。在结合TypeScript使用时,this.refModal.current
有可能是null的(在mounted之前使用会是null),需要在使用前加判断或者断言。
结合函数调用的方法,最终使用方法变成了:
class UserListView extends React.Component {
modal = createModal(UserModal)
// 打开新建或编辑窗口
onModalOpen = (id) => {
this.modal.show(id, (data) => {
// 窗口确定回调
// 返回true,阻止窗口关闭
})
}
render () {
const users = [/** 从后端获取 */]
<div>
<header>
<button onClick={() => this.onModalOpen()}>新建用户</button>
</header>
<ul>
{users.map((user) => (
<li key={user.id}>
<span>{user.name}</span>
<button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
</li>
))}
</ul>
<this.modal.Component /> {/** 渲染窗口组件 */}
</div>
}
}
结语
React的魅力就在于它给予了足够广阔的实现空间,你总能发现更优的解决方案,甚至是颠覆性的。
以上功能createModal
具体实现请查看gist,此外,通过结合React Hooks,还有一个新的方案@react-hero/modal,可以将<this.modal.Component />
都省略了,具体实现就不展开了。