Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hacker0limbo opened this issue Aug 28, 2020 · 1 comment
Open

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

hacker0limbo opened this issue Aug 28, 2020 · 1 comment
Labels
react react 笔记整理

Comments

@hacker0limbo
Copy link
Owner

引子

实习第一周遇到一个 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 还可能存在一些自己的状态, 所以我选择分别用在 actionreducer 基础上封装一层, 提供一个 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 里面的 opendialogType 属性选择性渲染. 这样保证了各种 Dialog 组件均在 App 的 context 下, 因此 Theme 也就跟随 App 了
  • openclose 方法全部使用 redux 里的 action, 保证状态的一致

总结

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

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

参考

@hacker0limbo hacker0limbo added the react react 笔记整理 label Aug 28, 2020
@WeiqiYang0704
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react 笔记整理
Projects
None yet
Development

No branches or pull requests

2 participants