Open hacker0limbo opened 4 years ago
实习第一周遇到一个 task 是重构一个 Dialog 组件, 看了一下项目代码发现有点东西, 原始代码我抽象了一下大致如下:
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. 简单讲一下原始代码的思路:
handleOpen
App
Reducer
我第一次遇到原来 Redux 还能这么玩... 毕竟正常 Reducer 里面应该存放可序列化的状态. 我搜了下, 发现还真有人提过这么一个类似问题: Storing React component in a Redux reducer?
很显然这么做肯定不好, 于是就让我重构了. Material UI 本身就有封装 Dialog 组件. 照着官方文档先改了一下:
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
NavBar
TopicDialog
props
本来想着这样重构就结束了, 但是测试时候发现样式不对. 具体问题为: 由于 TopicDialog 组件放置在 NavBar 组件下, 其主题(Theme) 会直接沿用上级组件, 比如这里的 NavBar 主题是暗色主题, 那么 TopciDialog 颜色什么的都是暗色, 但我想要的主题可能是亮色的
Theme
TopciDialog
未重构前的代码没出现这样的问题, 其实可以看到, {...componet} 渲染 Dialog 组件的时候, 该 Dialog 组件是放在级别比较高的 App 里面的, 不受 Navbar 控制
{...componet}
Navbar
于是问了我的 mentor, 提供了两个思路:
Redux
第一个方法很简单, 代码基本就是这样:
const TopicDialog = props => { const { open, onClose } = props return ( <ThemeProvider theme={theme}> <Dialog open={open} onClose={onClose}> ... </Dialog> </ThemeProvider> ) }
直接用 ThemeProvider 包裹一下, 我本身不熟悉 Material UI, 不过最后还是从项目里找到了亮色主题的 theme, 导入了进来
ThemeProvider
theme
第二种方法 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 } }
思路:
dialogType
action 部分
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
// 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') }) })
这里逻辑和代码有些复杂, 当然也可能是我写复杂了, 具体来说有以下几个点:
topicDialog
userDialog
dialog
suffix
combineReducer
const state = { // 其他 state dialog: { topic: { dialogType: 'topicDialog', open: true, }, user: { dialogType: 'userDialog', open: false, } } }
总结来讲, 通过 dialogType 来判断是哪种 dialog 类型, open 来控制每种类型的 Dialog 的显示隐藏
最后是每一个特定的 Dialog:
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 /> ) }
close
redux
最后还是老老实实选了官方那种(覆盖 theme 的), 因为我不想写这么多代码, 以及我觉得用 Redux 来保存 Dialog 的 state 有点大材小用了...
当然我这种封装可能也不对...
完
长知识了哈哈, react component 竟然还可以放到redux 里面哈哈
引子
实习第一周遇到一个 task 是重构一个
Dialog
组件, 看了一下项目代码发现有点东西, 原始代码我抽象了一下大致如下:先提一下, 公司技术栈为 React + Redux + Material UI. 简单讲一下原始代码的思路:
handleOpen
方法里的App
(实际项目里面可能是某个级别比较高的组件) 里面, 判断Reducer
存放的 Dialog 组件是否存在, 存在直接渲染我第一次遇到原来 Redux 还能这么玩... 毕竟正常
Reducer
里面应该存放可序列化的状态. 我搜了下, 发现还真有人提过这么一个类似问题: Storing React component in a Redux reducer?很显然这么做肯定不好, 于是就让我重构了. Material UI 本身就有封装
Dialog
组件. 照着官方文档先改了一下:重构 1
思路其实很简单:
open
来控制Dialog
组件的开关Dialog
组件不在作为一个抽象概念, 而是直接放在相关组件下边, 这里是放在NavBar
里, 可以看到这个TopicDialog
组件是比较定制化的props
传递给子组件本来想着这样重构就结束了, 但是测试时候发现样式不对. 具体问题为: 由于
TopicDialog
组件放置在NavBar
组件下, 其主题(Theme
) 会直接沿用上级组件, 比如这里的NavBar
主题是暗色主题, 那么TopciDialog
颜色什么的都是暗色, 但我想要的主题可能是亮色的未重构前的代码没出现这样的问题, 其实可以看到,
{...componet}
渲染 Dialog 组件的时候, 该 Dialog 组件是放在级别比较高的App
里面的, 不受Navbar
控制于是问了我的 mentor, 提供了两个思路:
TopicDialog
用自己的亮色的主题, 覆盖掉父级组件的主题Redux
第一个方法很简单, 代码基本就是这样:
直接用
ThemeProvider
包裹一下, 我本身不熟悉 Material UI, 不过最后还是从项目里找到了亮色主题的theme
, 导入了进来重构 2
第二种方法 mentor 没有讲具体的细节, 我按照自己的思路试了一下, 先看一下抽象组件
CustomDialog
, 大致如下:CustomDialog 部分
思路:
CustomDialog
是一个抽象组件, 也是按条件渲染.Redux
连接, 根据open
属性拿到目前需要显示的dialogType
, 渲染对应的Dialog
组件redux 部分:
action
部分reducer
部分这里逻辑和代码有些复杂, 当然也可能是我写复杂了, 具体来说有以下几个点:
Dialog
组件其实都有一些相同点, 比如都存在open
属性来控制显示. 不同的地方在于可能我叫topicDialog
, 你叫userDialog
, 然后每个dialog
还可能存在一些自己的状态, 所以我选择分别用在action
和reducer
基础上封装一层, 提供一个suffix
来区分不同的dialog
combineReducer
, 所以最后的状态可能长这样:总结来讲, 通过
dialogType
来判断是哪种 dialog 类型,open
来控制每种类型的Dialog
的显示隐藏最后是每一个特定的
Dialog
:TopicDialog 部分
思路:
CustomDialog
作为一个比较基础的公共组件, 根据 reducer 里面的open
和dialogType
属性选择性渲染. 这样保证了各种Dialog
组件均在App
的 context 下, 因此 Theme 也就跟随 App 了open
和close
方法全部使用redux
里的action
, 保证状态的一致总结
最后还是老老实实选了官方那种(覆盖 theme 的), 因为我不想写这么多代码, 以及我觉得用 Redux 来保存 Dialog 的 state 有点大材小用了...
当然我这种封装可能也不对...
完
参考