如何最大化 React 组件的可重用性
React 是一个流行的库,开发者可以使用它来构建高度复杂且交互性强的 Web 应用用户界面。许多开发者利用这个库构建应用,也因为许多原因觉得它很有趣。例如,它的声明式特性使得构建 Web 应用变得轻松有趣,因为代码可以变得更加可预测和可控。
那么怎样才能让它不那么痛苦呢?有哪些例子可以帮助说明如何使用 React 来构建高度复杂且交互式的用户界面?
本文将探讨如何最大限度地发挥 React 的可复用性,并提供一些您现在可以在 React 应用中运用的技巧和窍门。我们将通过构建一个真实的 React 组件来演示,并逐步解释为什么需要采取某些步骤以及如何提高这些步骤的可复用性。我想强调的是,有很多方法可以使组件可复用,虽然本文将解释一些重要的方法,但并未涵盖所有方法!
这篇文章适合初学者、中级和高级 React 开发人员——尽管它对初学者和中级开发人员更有用。
不用多说,我们开始吧!
组件
让我们构建一个列表组件并尝试从那里扩展其功能。
假设我们正在构建一个页面,用户注册成为医疗专业人员社区的成员后会被重定向到该页面。该页面应显示医生可以创建的群组列表,新注册的医生可以查看这些群组。每个列表应显示某种类型的标题、描述、群组创建者、代表其群组的图片以及一些基本信息,例如日期。
我们可以创建一个代表组的简单列表组件,如下所示:
function List(props) {
return (
<div>
<h5>
Group: <em>Pediatricians</em>
</h5>
<ul>
<p>Members</p>
<li>Michael Lopez</li>
<li>Sally Tran</li>
<li>Brian Lu</li>
<li>Troy Sakulbulwanthana</li>
<li>Lisa Wellington</li>
</ul>
</div>
)
}
然后我们可以轻松地渲染它并结束它:
import React from 'react'
import List from './List'
function App() {
return <List />
}
export default App
显然该组件是不可重复使用的,因此我们可以通过children的 props 提供一些基本的可重用性来解决该问题:
function List(props) {
return <div>{props.children}</div>
}
function App() {
return (
<List>
<h5>
Group: <em>Pediatricians</em>
</h5>
<ul>
<p>Members</p>
<li>Michael Lopez</li>
<li>Sally Tran</li>
<li>Brian Lu</li>
<li>Troy Sakulbulwanthana</li>
<li>Lisa Wellington</li>
</ul>
</List>
)
}
但这没什么意义,因为这个List
组件甚至不再是一个列表组件,甚至不应该被命名为列表,因为它现在只是一个返回div
元素的组件。我们不妨直接把代码移到App
组件里。但这很糟糕,因为现在我们把组件硬编码到了 中App
。如果我们确定列表是一次性使用的,这样做可能没问题。但我们知道会有多个列表,因为我们会用它来在网页上渲染不同的医疗组。
因此我们可以重构List
来为其列表元素提供更窄的 props:
function List({ groupName, members = [] }) {
return (
<div>
<h5>
Group: <em>{groupName}</em>
</h5>
<ul>
<p>Members</p>
{members.map((member) => (
<li key={member}>{member}</li>
))}
</ul>
</div>
)
}
这看起来好一点了,现在我们可以重新使用List
类似这样:
import React from 'react'
import './styles.css'
function App() {
const pediatricians = [
'Michael Lopez',
'Sally Tran',
'Brian Lu',
'Troy Sakulbulwanthana',
'Lisa Wellington',
]
const psychiatrists = [
'Miguel Rodriduez',
'Cassady Campbell',
'Mike Torrence',
]
return (
<div className="root">
<div className="listContainer">
<List groupName="Pediatricians" members={pediatricians} />
</div>
<div className="listContainer">
<List groupName="Psychiatrists" members={psychiatrists} />
</div>
</div>
)
}
export default App
这里没有太多关于样式的内容,但是为了避免混淆,将它们列在这里:
.root {
display: flex;
}
.listContainer {
flex-grow: 1;
}
一个仅限于此网页的小型应用可能只需要这个简单的组件即可。但是,如果我们处理的数据集可能很大,列表需要渲染数百行,该怎么办?最终,页面会尝试显示所有行,这可能会导致崩溃、卡顿、元素错位或重叠等问题。
这不是一个很好的用户体验,因此我们可以提供一种当成员数量达到一定数量时扩展列表的方法:
function List({ groupName, members = [] }) {
const [collapsed, setCollapsed] = React.useState(members.length > 3)
const constrainedMembers = collapsed ? members.slice(0, 3) : members
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
return (
<div>
<h5>
Group: <em>{groupName}</em>
</h5>
<ul>
<p>Members</p>
{constrainedMembers.map((member) => (
<li key={member}>{member}</li>
))}
{members.length > 3 && (
<li className="expand">
<button type="button" onClick={toggle}>
Expand
</button>
</li>
)}
</ul>
</div>
)
}
.root {
display: flex;
}
.listContainer {
flex-grow: 1;
box-sizing: border-box;
width: 100%;
}
li.expand {
list-style-type: none;
}
button {
border: 0;
border-radius: 4px;
padding: 5px 10px;
outline: none;
cursor: pointer;
}
button:active {
color: rgba(0, 0, 0, 0.75);
}
现在看来,我们已经获得了一个非常好的可重用组件来渲染组列表。
我们绝对可以做得更好。我们实际上不必专门为某个组织的群组使用这个组件。
如果我们可以把它用于其他用途呢?为标签提供一个 prop(在我们的例子中是Group
:) 可以从逻辑上实现这一点:
function List({ label, groupName, members = [] }) {
const [collapsed, setCollapsed] = React.useState(members.length > 3)
const constrainedMembers = collapsed ? members.slice(0, 3) : members
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
return (
<div>
<h5>
{label}: <em>{groupName}</em>
</h5>
<ul>
<p>Members</p>
{constrainedMembers.map((member) => (
<li key={member}>{member}</li>
))}
{members.length > 3 && (
<li className="expand">
<button type="button" onClick={toggle}>
Expand
</button>
</li>
)}
</ul>
</div>
)
}
然后您可以将其用于其他目的:
function App() {
return (
<div className="root">
<div className="listContainer">
<List
groupName="customerSupport"
members={['Lousie Yu', 'Morgan Kelly']}
/>
</div>
</div>
)
}
在思考如何让 React 组件更具可复用性时,一个简单但有效的方法是重新思考 prop 变量的命名方式。大多数情况下,一个简单的重命名就能带来巨大的改变。
因此,在我们的App
组件中,我们还可以为该部分提供自定义道具Members
:
function List({ label, labelValue, sublabel, members = [] }) {
const [collapsed, setCollapsed] = React.useState(members.length > 3)
const constrainedMembers = collapsed ? members.slice(0, 3) : members
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
return (
<div>
<h5>
{label}: <em>{labelValue}</em>
</h5>
<ul>
<p>{sublabel}</p>
{constrainedMembers.map((member) => (
<li key={member}>{member}</li>
))}
{members.length > 3 && (
<li className="expand">
<button type="button" onClick={toggle}>
Expand
</button>
</li>
)}
</ul>
</div>
)
}
现在,如果我们查看我们的组件并仅提供members
prop,让我们看看我们得到了什么:
我不知道您怎么想,但我在这里看到的是,该列表实际上可以用于任何事情!
我们可以重复使用相同的组件来表示排队等待下一次预约的专利:
或者我们可以在竞标拍卖中使用它:
不要低估变量命名的力量。一个简单的命名修正就能带来巨大的改变。
让我们回到代码。我们在扩展其可重用性方面做得相当不错。但在我看来,我们实际上可以做得更多。
因此,既然我们知道我们的List
组件可以兼容重用,并且原因完全不相关,那么我们现在可以决定将组件的各个部分分解为子组件,以支持不同的用例,如下所示:
function ListRoot({ children, ...rest }) {
return <div {...rest}>{children}</div>
}
function ListHeader({ children }) {
return <h5>{children}</h5>
}
function ListComponent({ label, items = [], limit = 0 }) {
const [collapsed, setCollapsed] = React.useState(items.length > 3)
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
const constrainedItems = collapsed ? items.slice(0, limit) : items
return (
<ul>
<p>{label}</p>
{constrainedItems.map((member) => (
<li key={member}>{member}</li>
))}
{items.length > limit && (
<li className="expand">
<button type="button" onClick={toggle}>
Expand
</button>
</li>
)}
</ul>
)
}
function List({ header, label, members = [], limit }) {
return (
<ListRoot>
<ListHeader>{header}</ListHeader>
<ListComponent label={label} items={members} limit={limit} />
</ListRoot>
)
}
从功能上来说,它的工作方式相同,但现在我们将不同的元素拆分为列表子组件。
这带来了一些好处:
- 我们现在可以分别测试每个组件
- 它变得更具可扩展性(维护、代码大小)
- 即使代码变得更大,它也变得更易读
- 使用以下技术通过记忆优化每个组件
React.memo
请注意,大多数实现细节保持不变,但现在更具可重用性。
你可能已经注意到,collapsed
状态被移到了 中ListComponent
。我们可以通过propsListComponent
将状态控制权移回父级,从而轻松地实现 的复用:
function ListComponent({ label, items = [], collapsed, toggle, limit, total }) {
return (
<ul>
<p>{label}</p>
{items.map((member) => (
<li key={member}>{member}</li>
))}
{total > limit && (
<li className="expand">
<button type="button" onClick={toggle}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
</li>
)}
</ul>
)
}
function List({ header, label, items = [], limit = 3 }) {
const [collapsed, setCollapsed] = React.useState(items.length > limit)
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
return (
<ListRoot>
<ListHeader>{header}</ListHeader>
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
/>
</ListRoot>
)
}
知道通过 propsListComponent
提供collapse
状态管理变得更加可重用,我们可以做同样的事情,List
以便使用我们组件的开发人员有权控制它:
function App() {
const [collapsed, setCollapsed] = React.useState(true)
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
const pediatricians = [
'Michael Lopez',
'Sally Tran',
'Brian Lu',
'Troy Sakulbulwanthana',
'Lisa Wellington',
]
const psychiatrists = [
'Miguel Rodriduez',
'Cassady Campbell',
'Mike Torrence',
]
const limit = 3
return (
<div className="root">
<div className="listContainer">
<List
collapsed={collapsed}
toggle={toggle}
header="Bids on"
label="Bidders"
items={pediatricians}
limit={limit}
/>
</div>
<div className="listContainer">
<List header="Bids on" label="Bidders" items={psychiatrists} />
</div>
</div>
)
}
function List({ collapsed, toggle, header, label, items = [], limit = 3 }) {
return (
<ListRoot>
<ListHeader>{header}</ListHeader>
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
/>
</ListRoot>
)
}
我们开始看到一个模式浮现出来。它似乎props
与可重用性有很大关系——没错!
实践中,开发者想要重写子组件的实现来提供自己的组件的情况并不少见。我们也可以List
通过 props 提供一个重写器来让我们的组件支持这种做法:
function List({
collapsed,
toggle,
header,
label,
items = [],
limit = 3,
renderHeader,
renderList,
}) {
return (
<ListRoot>
{renderHeader ? renderHeader() : <ListHeader>{header}</ListHeader>}
{renderList ? (
renderList()
) : (
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
/>
)}
</ListRoot>
)
}
这是一个非常常见且功能强大的模式,在许多 React 库中都有使用。在可重用性方面,始终保留默认实现至关重要。例如,如果开发人员想要覆盖 ,ListHeader
他可以通过传入 来提供自己的实现renderHeader
,否则它将默认渲染原始的ListHeader
。这是为了让列表组件保持功能一致且不可破坏。
但是,即使您在未使用覆盖器的情况下提供默认实现,也最好提供一种方法来删除或隐藏组件中的某些内容。
例如,如果我们想为开发人员提供一种完全不渲染任何标题元素的方法,那么通过 props提供一个“开关”就是一种很有用的策略。我们不想污染props 中的命名空间,所以我们可以重用props,这样如果他们传入它,就可以完全不渲染列表标题:header
null
function List({
collapsed,
toggle,
header,
label,
items = [],
limit = 3,
renderHeader,
renderList,
}) {
return (
<ListRoot>
{renderHeader ? (
renderHeader()
) : // HERE
header !== null ? (
<ListHeader>{header}</ListHeader>
) : null}
{renderList ? (
renderList()
) : (
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
/>
)}
</ListRoot>
)
}
<List
collapsed={collapsed}
toggle={toggle}
header={null} // Using the switch
label="Bidders"
items={pediatricians}
limit={limit}
/>
我们还可以进一步完善我们的可复用List
组件。我们并不局限于为ListHeader
and提供覆盖方法ListComponent
。我们还可以提供一种方法来覆盖根组件,如下所示:
function List({
component: RootComponent = ListRoot,
collapsed,
toggle,
header,
label,
items = [],
limit = 3,
renderHeader,
renderList,
}) {
return (
<RootComponent>
{renderHeader ? (
renderHeader()
) : header !== null ? (
<ListHeader>{header}</ListHeader>
) : null}
{renderList ? (
renderList()
) : (
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
/>
)}
</RootComponent>
)
}
请记住,当我们提供像这样的可定制选项时,我们总是默认使用默认实现,就像我们默认使用原始ListRoot
组件一样。
现在,父级可以轻松提供自己的时尚容器组件,将其呈现List
为其子级:
function App() {
const [collapsed, setCollapsed] = React.useState(true)
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
const pediatricians = [
'Michael Lopez',
'Sally Tran',
'Brian Lu',
'Troy Sakulbulwanthana',
'Lisa Wellington',
]
const psychiatrists = [
'Miguel Rodriduez',
'Cassady Campbell',
'Mike Torrence',
]
const limit = 3
function BeautifulListContainer({ children }) {
return (
<div
style={{
background: 'teal',
padding: 12,
borderRadius: 4,
color: '#fff',
}}
>
{children}
Today is: {new Date().toDateString()}
</div>
)
}
return (
<div className="root">
<div className="listContainer">
<List
component={BeautifulListContainer}
collapsed={collapsed}
toggle={toggle}
header={null}
label="Bidders"
items={pediatricians}
limit={limit}
/>
</div>
<div className="listContainer">
<List header="Bids on" label="Bidders" items={psychiatrists} />
</div>
</div>
)
}
有时开发人员也希望提供自己的列表行,因此,使用我们在这篇文章中讨论的相同概念,我们可以实现这一点。首先,让我们将li
元素抽象到它们自己的ListItem
组件中:
function ListComponent({ label, items = [], collapsed, toggle, limit, total }) {
return (
<ul>
<p>{label}</p>
{items.map((member) => (
<ListItem key={member}>{member}</ListItem>
))}
{total > limit && (
<ListItem className="expand">
<button type="button" onClick={toggle}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
</ListItem>
)}
</ul>
)
}
function ListItem({ children, ...rest }) {
return <li {...rest}>{children}</li>
}
然后更改List
以提供可自定义的渲染器来覆盖默认值ListItem
:
function List({
component: RootComponent = ListRoot,
collapsed,
toggle,
header,
label,
items = [],
limit = 3,
renderHeader,
renderList,
renderListItem,
}) {
return (
<RootComponent>
{renderHeader ? (
renderHeader()
) : header !== null ? (
<ListHeader>{header}</ListHeader>
) : null}
{renderList ? (
renderList()
) : (
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
renderListItem={renderListItem}
/>
)}
</RootComponent>
)
}
并稍微修改ListComponent
以支持该定制:
function ListComponent({
label,
items = [],
collapsed,
toggle,
limit,
total,
renderListItem,
}) {
return (
<ul>
<p>{label}</p>
{items.map((member) =>
renderListItem ? (
<React.Fragment key={member}>{renderListItem({ collapsed, toggle, member )}</React.Fragment>
) : (
<ListItem key={member}>{member}</ListItem>
),
)}
{total > limit && (
<ListItem className='expand'>
<button type='button' onClick={toggle}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
</ListItem>
)}
</ul>
)
}
注意:我们将对 的调用包装renderListItem(member)
在 a 中,React.Fragment
以便我们能够处理key
对 的赋值,这样用户就无需自己动手了。这个简单的改动能够显著提升试用我们组件的用户的好评,因为它省去了用户自行处理赋值的麻烦。
作为一名 React 开发者,我仍然看到很多机会可以最大化我们List
组件的可复用性,充分发挥其潜力。但由于文章篇幅过长,我打算最后再补充几个,作为结束,让你们开启这段旅程 :)
我想强调的是,充分利用渲染器 props (例如renderListItem
或 )renderHeader
将参数传递回调用者非常重要。这是一个强大的模式,也是为什么在 React Hooks 发布之前,render props 模式就被广泛采用的原因。
回到 prop 变量的命名,我们可以意识到这个组件实际上不需要每次都表示一个列表。我们可以让它兼容许多不同的情况,而不仅仅是渲染列表!我们真正需要关注的是组件在代码中的实现方式。
它本质上就是获取一个项目列表并渲染它们,同时支持折叠等炫酷功能。你可能觉得折叠功能只存在于下拉菜单、列表、菜单等组件中。但实际上,任何东西都可以折叠!我们组件中的任何功能并非仅限于这些组件。
例如,我们可以轻松地重用导航栏组件:
我们的组件本质上与以前相同,只是我们提供了更多的道具,如renderCollapser
和renderExpander
:
function ListComponent({
label,
items = [],
collapsed,
toggle,
limit,
total,
renderListItem,
renderCollapser,
renderExpander,
}) {
let expandCollapse
if (total > limit) {
if (collapsed) {
expandCollapse = renderExpander ? (
renderExpander({ collapsed, toggle })
) : (
<button type="button" onClick={toggle}>
Expand
</button>
)
} else {
expandCollapse = renderCollapser ? (
renderCollapser({ collapsed, toggle })
) : (
<button type="button" onClick={toggle}>
Collapse
</button>
)
}
}
return (
<ul>
<p>{label}</p>
{items.map((member) =>
renderListItem ? (
<React.Fragment key={member}>
{renderListItem({ collapsed, toggle, member })}
</React.Fragment>
) : (
<ListItem key={member}>{member}</ListItem>
),
)}
{total > limit && (
<ListItem className="expand">{expandCollapse}</ListItem>
)}
</ul>
)
}
function ListItem({ children, ...rest }) {
return <li {...rest}>{children}</li>
}
function List({
component: RootComponent = ListRoot,
collapsed,
toggle,
header,
label,
items = [],
limit = 3,
renderHeader,
renderList,
renderListItem,
renderCollapser,
renderExpander,
}) {
return (
<RootComponent>
{renderHeader ? (
renderHeader()
) : header !== null ? (
<ListHeader>{header}</ListHeader>
) : null}
{renderList ? (
renderList()
) : (
<ListComponent
label={label}
items={
collapsed && items.length > limit ? items.slice(0, limit) : items
}
collapsed={collapsed}
toggle={toggle}
limit={limit}
total={items.length}
renderListItem={renderListItem}
renderCollapser={renderCollapser}
renderExpander={renderExpander}
/>
)}
</RootComponent>
)
}
function App() {
const [collapsed, setCollapsed] = React.useState(true)
function toggle() {
setCollapsed((prevValue) => !prevValue)
}
const pediatricians = ['Home', 'Posts', 'About', 'More', 'Contact', 'FAQ']
const limit = 3
function renderCollapser({ collapsed, toggle }) {
return <ChevronLeftIcon onClick={toggle} />
}
function renderExpander({ collapsed, toggle }) {
return <ChevronRightIcon onClick={toggle} />
}
function renderListItem({ collapsed, toggle, member }) {
function onClick() {
window.alert(`Clicked ${member}`)
}
return (
<li className="custom-li" onClick={onClick}>
{member}
</li>
)
}
return (
<div className="navbar">
<div className="listContainer">
<List
collapsed={collapsed}
toggle={toggle}
header={null}
items={pediatricians}
limit={limit}
renderCollapser={renderCollapser}
renderExpander={renderExpander}
renderListItem={renderListItem}
/>
</div>
</div>
)
}
这就是最大化可重用性的力量!
结论
这篇文章到此结束!希望您觉得这篇文章有价值,并期待未来有更多内容。
鏂囩珷鏉ユ簮锛�https://dev.to/jsmanifest/how-to-maximize-reusability-for-your-react-components-875