发布于 2026-01-06 3 阅读
0

React中简化大型组件的力量

React中简化大型组件的力量

在Medium上找到我

拥有大型组件并不总是坏事,但最好抓住机会进一步简化组件,尤其是在这样做能带来额外好处的情况下。

当组件体积较大时,可能会出现不利情况,因为组件越大,随着时间的推移,维护和读取就越困难。

让我们来看一下下面的这个组件,并了解一下为什么简化它会更好。

(这是生产环境中的应用程序代码,所以这是一个真实的示例)

以下组件SidebarSection接收一些 props,其中props.ids一个是字符串形式的商品 ID 数组,另一个是使用每个商品的ID 作为键来props.items映射侧边栏商品的对象。它使用这些 props 来渲染侧边栏商品:id

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import List from '@material-ui/core/List'
import Divider from '@material-ui/core/Divider'
import ListSubheader from '@material-ui/core/ListSubheader'
import { EDIT_NOTEBOOK, DELETE_NOTEBOOK } from 'features/toplevel'
import { selectSelected } from 'features/sidebar'
import SidebarContext from './SidebarContext'
import SidebarItem from './SidebarItem'

function SidebarSection({ id: sectionId, ids, items, depth, expanded }) {
  const ctx = React.useContext(SidebarContext)
  const selectedId = useSelector(selectSelected)

  if (!ctx) return null

  return (
    <List dense={depth > 0} disablePadding>
      {ids.map((id: string, itemIndex: number) => {
        const key = `SidebarSection_${id}_item${itemIndex}`
        const item = items[id]

        switch (item.type) {
          case 'divider':
            return <Divider key={key} style={{ padding: 0, margin: 0 }} />
          case 'label':
            return (
              <ListSubheader
                key={key}
                style={{
                  transform: expanded ? undefined : 'scale(0.55)',
                  textOverflow: 'ellipsis',
                  overflow: 'hidden',
                  userSelect: 'none',
                }}
                disableGutters={!expanded}
              >
                {item.label}
              </ListSubheader>
            )
          case 'notebook': {
            // Called inside unserializeHoverControlsProps when iterating over each hover action
            const onHoverAction = (action: any) => {
              if (action.Icon) {
                const notebook = item.data
                if (notebook) {
                  action.onClick = ctx.createHoverControlsActionOnClick({
                    context:
                      action.name === 'edit'
                        ? EDIT_NOTEBOOK
                        : action.name === 'delete'
                        ? DELETE_NOTEBOOK
                        : '',
                    data:
                      action.name === 'edit'
                        ? item
                        : action.name === 'delete'
                        ? {
                            id: notebook.id,
                            title: notebook.info.title,
                            description: notebook.info.description,
                            isEncrypt: notebook.isEncrypt,
                            created_at: notebook.created_at,
                            modified_at: notebook.modified_at,
                          }
                        : null,
                  })
                }
              }
            }

            return (
              <SidebarItem
                key={key}
                sectionId={sectionId}
                depth={depth}
                item={ctx.unserializeItem(item, { onHoverAction })}
                isSelected={item.id === selectedId}
                {...ctx}
              />
            )
          }
          case 'static':
            return (
              <SidebarItem
                key={key}
                sectionId={sectionId}
                depth={depth}
                item={ctx.unserializeItem(item)}
                isSelected={item.id === selectedId}
                {...ctx}
              />
            )
          default:
            return null
        }
      })}
    </List>
  )
}
Enter fullscreen mode Exit fullscreen mode

这个组件看起来其实还不错但仔细想想,每次我们编辑这个组件时,都必须先理解每一行代码才能进行更改,因为我们不知道更改某些内容是否会破坏组件的其他部分。

例如,onHoverAction在 switch 语句中创建的函数。它不必要地使组件变得臃肿,并且根据SidebarItem其实现方式的不同,可能会导致无限循环,因为每次组件重新渲染时都会重新创建对该函数的引用。

这也使得整个组件对单元测试更加敏感,因为我们将SidebarSection实现细节的责任委托给了组件。在单元测试中,当我们测试组件行为是否正确时onHoverAction,就必须了解实现细节,这意义不大(这意味着要注意语法错误之类的问题,因为函数内部的拼写错误可能会导致渲染失败,而我们会因此责怪组件工作不力)。onHoverActionSidebarSectionSidebarSection

我们可以将其提取到外部来简化这个问题,这样我们就无需再将责任归咎于组件了:

function onHoverAction(item, createOnClick) {
  return (action) => {
    if (action.Icon) {
      const notebook = item.data
      if (notebook) {
        action.onClick = ctx.createHoverControlsActionOnClick({
          context:
            action.name === 'edit'
              ? EDIT_NOTEBOOK
              : action.name === 'delete'
              ? DELETE_NOTEBOOK
              : '',
          data:
            action.name === 'edit'
              ? item
              : action.name === 'delete'
              ? {
                  id: notebook.id,
                  title: notebook.info.title,
                  description: notebook.info.description,
                  isEncrypt: notebook.isEncrypt,
                  created_at: notebook.created_at,
                  modified_at: notebook.modified_at,
                }
              : null,
        })
      }
    }
  }
}

function SidebarSection({ id: sectionId, ids, items, depth, expanded }) {
  const ctx = React.useContext(SidebarContext)
  const selectedId = useSelector(selectSelected)

  if (!ctx) return null

  return (
    <List dense={depth > 0} disablePadding>
      {ids.map((id: string, itemIndex: number) => {
        const key = `SidebarSection_${id}_item${itemIndex}`
        const item = items[id]

        switch (item.type) {
          case 'divider':
            return <Divider key={key} style={{ padding: 0, margin: 0 }} />
          case 'label':
            return (
              <ListSubheader
                key={key}
                style={{
                  transform: expanded ? undefined : 'scale(0.55)',
                  textOverflow: 'ellipsis',
                  overflow: 'hidden',
                  userSelect: 'none',
                }}
                disableGutters={!expanded}
              >
                {item.label}
              </ListSubheader>
            )
          case 'notebook': {
            return (
              <SidebarItem
                key={key}
                sectionId={sectionId}
                depth={depth}
                item={ctx.unserializeItem(item, {
                  onHoverAction: onHoverAction(
                    item,
                    ctx.createHoverControlsActionOnClick,
                  ),
                })}
                isSelected={item.id === selectedId}
                {...ctx}
              />
            )
          }
          case 'static':
            return (
              <SidebarItem
                key={key}
                sectionId={sectionId}
                depth={depth}
                item={ctx.unserializeItem(item)}
                isSelected={item.id === selectedId}
                {...ctx}
              />
            )
          default:
            return null
        }
      })}
    </List>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们只是把这个功能移到了另一个地方,就几乎没费什么力气就给我们带来了巨大的好处:

  1. 对该函数的引用将保持不变。
  2. 现在它SidebarSection可以过上平静的生活了,因为它不再需要担心onHoverAction正确实现的问题。它只需要传递onHoverAction所需的参数即可。
  3. 现在我们可以onHoverAction单独进行单元测试了,因为它已经可以导出。想看看它是否按预期工作吗?只需导入它,传入三个参数,看看它返回什么即可。
  4. SidebarSection更易于阅读和维护。

实际上,我们还可以做更多简化工作。我们还有机会进一步简化这个组件。这两个 switch 语句块中存在重复代码:

case 'notebook':
  return (
    <SidebarItem
      key={key}
      sectionId={sectionId}
      depth={depth}
      item={ctx.unserializeItem(item, {
        onHoverAction: onHoverAction(
          action,
          item,
          ctx.createHoverControlsActionOnClick,
        ),
      })}
      isSelected={item.id === selectedId}
      {...ctx}
    />
  )
case 'static':
  return (
    <SidebarItem
      key={key}
      sectionId={sectionId}
      depth={depth}
      item={ctx.unserializeItem(item)}
      isSelected={item.id === selectedId}
      {...ctx}
    />
  )
Enter fullscreen mode Exit fullscreen mode

实际上,保持现状可能不会造成太大问题。但是,我确信任何阅读这段代码的开发者都会忍不住逐行检查每个属性,以确保它们之间没有太大区别。

毕竟,理想情况下,我们希望看到相似的代码被分开是有重要原因的,那么究竟是什么原因导致它们被分开呢?就我们的情况而言,并没有什么特别充分的理由,所以最好简化一下,以免未来的开发者在调试这个组件时遇到同样的尴尬情况。

我们可以通过以下方式简化这个问题:

case 'notebook':
case 'static':
  return (
    <SidebarItem
      key={key}
      sectionId={sectionId}
      depth={depth}
      item={ctx.unserializeItem(item, item.type === 'notebook' ? {
        onHoverAction: onHoverAction(
          action,
          item,
          ctx.createHoverControlsActionOnClick,
        ),
      } : undefined)}
      isSelected={item.id === selectedId}
      {...ctx}
    />
  )

Enter fullscreen mode Exit fullscreen mode

简化后,这带来了一些重要的好处:

  1. 我们删除了重复代码。
  2. 现在更容易阅读了,因为我们只需要查看代码的一个“副本”。
  3. 自文档化代码(它基本上告诉我们类型为“notebook”“static”'notebook'的项几乎完全相同,除了类型为“notebook”的项可以点击而类型为“static”的项不可以点击之外,无需过多担心它们之间的差异'static')。

简化反而导致过度思考

现在我们还有一些地方可以“简化”。虽然我们的 switch 语句缩短了一些,但看起来有点丑陋。以下是SidebarSection应用简化更改后的组件:

function SidebarSection({ id: sectionId, ids, items, depth, expanded }) {
  const ctx = React.useContext(SidebarContext)
  const selectedId = useSelector(selectSelected)

  if (!ctx) return null

  return (
    <List dense={depth > 0} disablePadding>
      {ids.map((id: string, itemIndex: number) => {
        const key = `SidebarSection_${id}_item${itemIndex}`
        const item = items[id]

        switch (item.type) {
          case 'divider':
            return <Divider key={key} style={{ padding: 0, margin: 0 }} />
          case 'label':
            return (
              <ListSubheader
                key={key}
                style={{
                  transform: expanded ? undefined : 'scale(0.55)',
                  textOverflow: 'ellipsis',
                  overflow: 'hidden',
                  userSelect: 'none',
                }}
                disableGutters={!expanded}
              >
                {item.label}
              </ListSubheader>
            )
          case 'notebook':
          case 'static':
            return (
              <SidebarItem
                key={key}
                sectionId={sectionId}
                depth={depth}
                item={ctx.unserializeItem(
                  item,
                  item.type === 'notebook'
                    ? {
                        onHoverAction: onHoverAction(
                          action,
                          item,
                          ctx.createHoverControlsActionOnClick,
                        ),
                      }
                    : undefined,
                )}
                isSelected={item.id === selectedId}
                {...ctx}
              />
            )

          default:
            return null
        }
      })}
    </List>
  )
}
Enter fullscreen mode Exit fullscreen mode

这里可能出现的一个问题是,我们赋予每个项目的渲染块过多的责任,使其负责将正确的属性传递给正确的组件。

这样看来,或许改写成这样会更好:

function getProps({ item, expanded, sectionId, selectedId, depth, ctx }) {
  switch (item.type) {
    case 'divider':
      return { style: { padding: 0, margin: 0 } }
    case 'label':
      return {
        style: {
          transform: expanded ? undefined : 'scale(0.55)',
          textOverflow: 'ellipsis',
          overflow: 'hidden',
          userSelect: 'none',
        },
        disableGutters: !expanded,
      }
    case 'notebook':
    case 'static':
      return {
        sectionId,
        depth,
        item: ctx.unserializeItem(
          item,
          item.type === 'notebook'
            ? {
                onHoverAction: onHoverAction(
                  item,
                  ctx.createHoverControlsActionOnClick,
                ),
              }
            : undefined,
        ),
        isSelected: item.id === selectedId,
        ...ctx,
      }
    default:
      return undefined
  }
}

function SidebarSection({ id: sectionId, ids, items, depth, expanded }) {
  const ctx = React.useContext(SidebarContext)
  const selectedId = useSelector(selectSelected)

  if (!ctx) return null

  return (
    <List dense={depth > 0} disablePadding>
      {ids.map((id: string, itemIndex: number) => {
        const key = `SidebarSection_${id}_item${itemIndex}`
        const item = items[id]

        let Component

        if (item.type === 'divider') {
          Component = Divider
        } else if (item.type === 'label') {
          Component = ListSubheader
        } else if (['notebook', 'static'].includes(item.type)) {
          Component = SidebarItem
        } else {
          return null
        }

        return (
          <Component
            key={key}
            {..getProps(
              item,
              expanded,
              sectionId,
              selectedId,
              depth,
              ctx
            })}
          />
        )
      })}
    </List>
  )
}
Enter fullscreen mode Exit fullscreen mode

现在我们进一步简化了代码SidebarSection,使其仅负责调用getProps以提供相关的 props 并Component根据条件分配正确的属性item.type。现在我们可以进行单元测试,getProps以确保它们根据条件返回正确的 props item.type

这次简化 React 代码的尝试是否成功?让我们来看看它带来的好处和带来的弊端:

好处:

  1. SidebarSection减少了其职责范围。
  2. SidebarSection变小了。
  3. 我们可以清楚地看到哪些 props 被注入到哪些组件中。
  4. 我们现在不必传球key={key}四次,只需像这样传球即可。<Component key={key}

缺点:

  1. SidebarSection文件变小了,但文件却变大了。
  2. 一个“实体”(所有内容都在其中SidebarSection)变成了三个“实体”(现在分别分离SidebarSection,,onHoverActiongetProps
  3. 为了看完所有内容,我们需要从上到下滚动鼠标,这样会给鼠标造成更大的压力。

所以,这样做值得吗?

说实话,我认为如果最后一部分耗时太长,那可能就得不偿失了。所以说,如果简化代码不需要花费太多精力,却能带来更多好处,那绝对值得去做

所以就我们这篇文章而言,我支持这篇文章中的前两种简化尝试,而对于第三种尝试我则有些犹豫。

然而,我们现在已经见识到了简化 React 中大型组件的力量。

结论

这篇文章就到此结束了!希望这篇文章对您有所帮助,敬请期待我未来的更多内容!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/the-power-of-simplifying-large-components-in-react-583k