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>
)
}
这个组件看起来其实还不错,但仔细想想,每次我们编辑这个组件时,都必须先理解每一行代码才能进行更改,因为我们不知道更改某些内容是否会破坏组件的其他部分。
例如,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>
)
}
我们只是把这个功能移到了另一个地方,就几乎没费什么力气就给我们带来了巨大的好处:
- 对该函数的引用将保持不变。
- 现在它
SidebarSection可以过上平静的生活了,因为它不再需要担心onHoverAction正确实现的问题。它只需要传递onHoverAction所需的参数即可。 - 现在我们可以
onHoverAction单独进行单元测试了,因为它已经可以导出。想看看它是否按预期工作吗?只需导入它,传入三个参数,看看它返回什么即可。 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}
/>
)
实际上,保持现状可能不会造成太大问题。但是,我确信任何阅读这段代码的开发者都会忍不住逐行检查每个属性,以确保它们之间没有太大区别。
毕竟,理想情况下,我们希望看到相似的代码被分开是有重要原因的,那么究竟是什么原因导致它们被分开呢?就我们的情况而言,并没有什么特别充分的理由,所以最好简化一下,以免未来的开发者在调试这个组件时遇到同样的尴尬情况。
我们可以通过以下方式简化这个问题:
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}
/>
)
简化后,这带来了一些重要的好处:
- 我们删除了重复代码。
- 现在更容易阅读了,因为我们只需要查看代码的一个“副本”。
- 自文档化代码(它基本上告诉我们类型为“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>
)
}
这里可能出现的一个问题是,我们赋予每个项目的渲染块过多的责任,使其负责将正确的属性传递给正确的组件。
这样看来,或许改写成这样会更好:
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>
)
}
现在我们进一步简化了代码SidebarSection,使其仅负责调用getProps以提供相关的 props 并Component根据条件分配正确的属性item.type。现在我们可以进行单元测试,getProps以确保它们根据条件返回正确的 props item.type。
这次简化 React 代码的尝试是否成功?让我们来看看它带来的好处和带来的弊端:
好处:
SidebarSection减少了其职责范围。SidebarSection变小了。- 我们可以清楚地看到哪些 props 被注入到哪些组件中。
- 我们现在不必传球
key={key}四次,只需像这样传球即可。<Component key={key}
缺点:
SidebarSection文件变小了,但文件却变大了。- 一个“实体”(所有内容都在其中
SidebarSection)变成了三个“实体”(现在分别分离为SidebarSection,,onHoverAction)getProps - 为了看完所有内容,我们需要从上到下滚动鼠标,这样会给鼠标造成更大的压力。
所以,这样做值得吗?
说实话,我认为如果最后一部分耗时太长,那可能就得不偿失了。所以说,如果简化代码不需要花费太多精力,却能带来更多好处,那绝对值得去做。
所以就我们这篇文章而言,我支持这篇文章中的前两种简化尝试,而对于第三种尝试我则有些犹豫。
然而,我们现在已经见识到了简化 React 中大型组件的力量。
结论
这篇文章就到此结束了!希望这篇文章对您有所帮助,敬请期待我未来的更多内容!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/the-power-of-simplifying-large-components-in-react-583k