Description
引子
实习第一周遇到一个 task 是重构一个 Dialog
组件, 看了一下项目代码发现有点东西, 原始代码我抽象了一下大致如下:
const NavBar = () => {
const handleOpen = () => {
const Dialog = (
<Dialog>
...
</Dialog>
)
dispatch(openDialog(Dialog))
}
return (
<Button onClick={handleOpen}>Open Dialog</Button>
)
}
const App = () => {
const { component } = useSelector(state => state.dialog)
return (
<div>
{component && ...component}
<div>
)
}
// actions
const openDialog = component => {
return {
type: 'OPEN_DIALOG',
payload: {
component
}
}
}
const closeDialog = () => {
return {
type: 'CLOSE_DIALOG',
}
}
// reducer
const dialog = (state={ component: null }, action) => {
switch(action.type) {
case 'OPEN_DIALOG':
return {
component: action.payload.component
}
case 'CLOSE_DIALOG':
return {
component: null
}
default:
return state
}
}
先提一下, 公司技术栈为 React + Redux + Material UI. 简单讲一下原始代码的思路:
- 用 Redux 存取 Dialog 组件数据, 即 Reducer 里面存的不是状态, 是一个 React 组件的虚拟 DOM
- open 和 close 行为均通过 Redux 触发, 其中 open 携带的数据即为对应的 Dialog 组件, 可以看到代码中 Dialog 组件是写在
handleOpen
方法里的 - 在
App
(实际项目里面可能是某个级别比较高的组件) 里面, 判断Reducer
存放的 Dialog 组件是否存在, 存在直接渲染
我第一次遇到原来 Redux 还能这么玩... 毕竟正常 Reducer
里面应该存放可序列化的状态. 我搜了下, 发现还真有人提过这么一个类似问题: Storing React component in a Redux reducer?
很显然这么做肯定不好, 于是就让我重构了. Material UI 本身就有封装 Dialog
组件. 照着官方文档先改了一下:
重构 1
const TopicDialog = props => {
const { open, onClose } = props
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Title</DialogTitle>
<DialogContent>
Content
</DialogContent>
<DialogActions>
DialogActions
</DialogActions>
</Dialog>
)
}
const NavBar = props => {
const [open, setOpen] = useState(false)
const handleOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
return (
<Button onClick={handleOpen}>Open Dialog<Button>
<TopicDialog open={open} onClose={handleClose} />
)
}
思路其实很简单:
- 通过一个状态
open
来控制Dialog
组件的开关 Dialog
组件不在作为一个抽象概念, 而是直接放在相关组件下边, 这里是放在NavBar
里, 可以看到这个TopicDialog
组件是比较定制化的- 结合 1, 2 两点来看, 状态与相关方法一般是通过父级组件来维护, 通过
props
传递给子组件
本来想着这样重构就结束了, 但是测试时候发现样式不对. 具体问题为: 由于 TopicDialog
组件放置在 NavBar
组件下, 其主题(Theme
) 会直接沿用上级组件, 比如这里的 NavBar
主题是暗色主题, 那么 TopciDialog
颜色什么的都是暗色, 但我想要的主题可能是亮色的
未重构前的代码没出现这样的问题, 其实可以看到, {...componet}
渲染 Dialog 组件的时候, 该 Dialog 组件是放在级别比较高的 App
里面的, 不受 Navbar
控制
于是问了我的 mentor, 提供了两个思路:
TopicDialog
用自己的亮色的主题, 覆盖掉父级组件的主题- 用
Redux
第一个方法很简单, 代码基本就是这样:
const TopicDialog = props => {
const { open, onClose } = props
return (
<ThemeProvider theme={theme}>
<Dialog open={open} onClose={onClose}>
...
</Dialog>
</ThemeProvider>
)
}
直接用 ThemeProvider
包裹一下, 我本身不熟悉 Material UI, 不过最后还是从项目里找到了亮色主题的 theme
, 导入了进来
重构 2
第二种方法 mentor 没有讲具体的细节, 我按照自己的思路试了一下, 先看一下抽象组件 CustomDialog
, 大致如下:
CustomDialog 部分
const CustomDialog = props => {
const { dialogType } = useSelector(state => {
const openedDialog = Object.entries(state.dialog)
.filter(([dialogName, dialogState]) => dialogState.open === true)[0]
return {
dialogType: dialogType[1]['dialogType']
}
})
switch(dialogType) {
case 'topicDialog':
return <TopicDialog />
case 'userDialog':
return <UserDialog />
default:
return null
}
}
思路:
CustomDialog
是一个抽象组件, 也是按条件渲染.- 和
Redux
连接, 根据open
属性拿到目前需要显示的dialogType
, 渲染对应的Dialog
组件
redux 部分:
action
部分
// action
const openDialog = dialogType => {
return {
type: 'OPEN',
payload: {
dialogType
}
}
}
const closeDialog = dialogType => {
return {
type: 'CLOSE',
payload: {
dialogType
}
}
}
// high order action creator
const withSuffixAction = (action, suffix) => {
return dialogType => {
const state = action(dialogType)
return {
...state,
type: `${state.type}_${suffix}`
}
}
}
export const openTopicDialog = withSuffixAction(openDialog, 'TOPIC_DIALOG')
export const closeTopicDialog = withSuffixAction(closeDialog, 'TOPIC_DIALOG')
reducer
部分
// reducer
const topicDialog = (state, action) => {
return state
}
const withSuffixReducer = (reducer, suffix) => {
return (state={ open: false }, action) => {
switch(action.type) {
case `OPEN_${suffix}`:
return {
...state,
open: true
dialogType: action.payload.dialogType
}
case `CLOSE_${suffix}`:
return {
...state,
open: false,
dialogType: action.payload.dialogType
}
default:
return reducer(state, action)
}
}
}
export const rootReducer = combineReducer({
//... 其他 reducer
dialog: combineReducer({
topic: withSuffixReducer(topicDialog, 'TOPIC_DIALOG')
})
})
这里逻辑和代码有些复杂, 当然也可能是我写复杂了, 具体来说有以下几个点:
- 由于不同
Dialog
组件其实都有一些相同点, 比如都存在open
属性来控制显示. 不同的地方在于可能我叫topicDialog
, 你叫userDialog
, 然后每个dialog
还可能存在一些自己的状态, 所以我选择分别用在action
和reducer
基础上封装一层, 提供一个suffix
来区分不同的dialog
- 使用嵌套的
combineReducer
, 所以最后的状态可能长这样:
const state = {
// 其他 state
dialog: {
topic: {
dialogType: 'topicDialog',
open: true,
},
user: {
dialogType: 'userDialog',
open: false,
}
}
}
总结来讲, 通过 dialogType
来判断是哪种 dialog 类型, open
来控制每种类型的 Dialog
的显示隐藏
最后是每一个特定的 Dialog
:
TopicDialog 部分
const TopicDialog = props => {
const { open } = useSelector(state => state.dialog.topic.open)
const dispatch = useDispatch()
const handleClose = () => {
dispatch(closeTopicDialog('topicDialog'))
}
return (
<Dialog open={open} onClose={handleClose}>
...
</Dialog>
)
}
const NavBar = props => {
const dispatch = useDispatch()
const handleTopicDialogOpen = () => {
dispatch(openTopicDialog('topicDialog'))
}
return (
<Button onClick={handleTopicDialogOpen}>Open Dialog</Button>
)
}
const App = props => {
return (
...
<CustomDialog />
)
}
思路:
- 在 UI 上和最开始项目的结构基本一致,
CustomDialog
作为一个比较基础的公共组件, 根据 reducer 里面的open
和dialogType
属性选择性渲染. 这样保证了各种Dialog
组件均在App
的 context 下, 因此 Theme 也就跟随 App 了 open
和close
方法全部使用redux
里的action
, 保证状态的一致
总结
最后还是老老实实选了官方那种(覆盖 theme 的), 因为我不想写这么多代码, 以及我觉得用 Redux 来保存 Dialog 的 state 有点大材小用了...
当然我这种封装可能也不对...
完