hacker0limbo / my-blog

个人博客
5 stars 1 forks source link

简单聊一聊一个 Dialog 组件的重构 #10

Open hacker0limbo opened 4 years ago

hacker0limbo commented 4 years ago

引子

实习第一周遇到一个 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 还能这么玩... 毕竟正常 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} />
  )
}

思路其实很简单:

本来想着这样重构就结束了, 但是测试时候发现样式不对. 具体问题为: 由于 TopicDialog 组件放置在 NavBar 组件下, 其主题(Theme) 会直接沿用上级组件, 比如这里的 NavBar 主题是暗色主题, 那么 TopciDialog 颜色什么的都是暗色, 但我想要的主题可能是亮色的

未重构前的代码没出现这样的问题, 其实可以看到, {...componet} 渲染 Dialog 组件的时候, 该 Dialog 组件是放在级别比较高的 App 里面的, 不受 Navbar 控制

于是问了我的 mentor, 提供了两个思路:

第一个方法很简单, 代码基本就是这样:

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
  }
}

思路:

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')
  })
})

这里逻辑和代码有些复杂, 当然也可能是我写复杂了, 具体来说有以下几个点:

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 />
  )
}

思路:

总结

最后还是老老实实选了官方那种(覆盖 theme 的), 因为我不想写这么多代码, 以及我觉得用 Redux 来保存 Dialog 的 state 有点大材小用了...

当然我这种封装可能也不对...

参考

WeiqiYang0704 commented 9 months ago

长知识了哈哈, react component 竟然还可以放到redux 里面哈哈