React 反模式:renderThing
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
如果你对 React 有相当的了解,你可能遇到过这种情况:
class Tabs extends React.Component {
constructor(props){
super(props)
this.state = {}
}
setActiveTab(activeTab){
this.setState({ activeTab });
}
renderTabs(){
return (
this.props.tabs.map(tab =>(
<a onClick={e => this.setActiveTab(tab.id)}
key={tab.id}
className={this.state.activeTab == tab.id ? "active" : ""}
>
{tab.title}
</a>
))
)
}
render(){
return (
<div>
<p>Choose an item</p>
<p>Current id: {this.state.activeTab}</p>
<nav>
{this.renderTabs()}
</nav>
</div>
)
}
}
它的用法如下:
<Tabs tabs={[{title: "Tab One", id: "tab-one"}, {title: "Tab Two", id: "tab-two"}]} />
这样就行了!如果这就是你对这个组件的全部需求,那么完全可以到此为止了!
但如果这段代码将来会发生变化,你最终可能会得到一个复杂冗长的组件。
这里最明显也是最先出现的重构问题在于这个renderTabs方法。它存在一些问题。
首先,Tabs组件已经有一个方法了。那么这两个方法render有什么区别呢?一个方法用于渲染标签列表,另一个方法用于添加一些上下文信息。这种情况在筛选列表之类的场景中很常见。Tabs renderrenderTabs
由于选项卡需要以某种方式与包含上下文共享状态,因此人们可能会倾向于将这种渲染功能封装在组件内部。
我们来想想如何重构这段代码,使其更容易理解。
PS:假设你已经制定了某种测试策略。在这种情况下,我们不会编写测试,但如果你需要编写测试,你可能需要断言你的列表能够正常渲染,以及点击标签页后会显示你想要显示的内容。
我们先来移除 renderTabs 方法。一开始看起来可能会不太美观。
class Tabs extends React.Component {
constructor(props){
super(props)
this.state = {}
}
setActiveTab(activeTab){
this.setState({ activeTab });
}
render(){
return (
<div>
<p>Choose an item</p>
<p>Current id: {this.state.activeTab}</p>
<nav>
{this.props.tabs.map(tab =>(
<a onClick={e => this.setActiveTab(tab.id)}
key={tab.id}
className={this.state.activeTab == tab.id ? "active" : ""}
>
{tab.title}
</a>
))}
</nav>
</div>
)
}
}
这本身其实是一个非常好的组件。但将来你可能需要在其他地方使用同样的标签式按钮,所以我们来看看能不能让这个按钮可以共享。
我们单独来看一个标签页。
<a onClick={e => this.setActiveTab(tab.id)}
key={tab.id}
className={this.state.activeTab == tab.id ? "active" : ""}
>
{tab.title}
</a>
现在让我们把这个组件做成一个独立的函数式组件。(换句话说,我们希望这个组件接收 props,但不需要它拥有自己的状态。)
const TabButton = ({ onClick, active, title, tabId, ...props}) => (
<a onClick={e => {e.preventDefault(); props.onClick(tabId)}}
{...props}
className={active ? "active" : ""}
>
{title}
</a>
)
现在我们有了一个功能组件,我们可以将其集成回我们原来的 Tabs 组件中。
const TabButton = ({ onClick, active, title, tabId, ...props}) => (
<a onClick={e => {e.preventDefault(); props.onClick(tabId)}}
{...props}
className={active ? "active" : ""}
>
{title}
</a>
)
class Tabs extends React.Component {
constructor(props){
super(props)
this.state = {}
}
setActiveTab(activeTab){
this.setState({ activeTab });
}
render(){
const { tabs } = this.props;
const { activeTab } = this.state;
return (
<div>
<p>Choose an item</p>
<p>Current id: {this.state.activeTab}</p>
<nav>
{this.props.tabs.map(tab =>(
<TabButton onClick={this.setActiveTab}
active={activeTab == tab.id}
tabId={tab.id}
key={tab.id}
title={tab.title}
/>
))}
</nav>
</div>
)
}
}
那么我们究竟能从中获得什么好处呢?
- 移除了不必要/令人困惑的 renderTabs 按钮
- 创建了一个可重用的 TabButton 组件,它不依赖任何外部状态。
Tabs接口API 没有变化- 两个较小的组件比一个较大的组件更容易进行推理和分离关注点。
这个例子虽然人为设计且规模较小,但你几乎肯定会找到renderThing怪物出没的地方。
重构模式如下所示:
- 将怪物
renderThing方法的代码移回原始渲染器中即可。如果代码合理,到此为止即可。 - 从渲染输出中提取一部分作为新组件。(请注意,您可以直接跳到此步骤,跳过步骤 1,但我喜欢先将其移回渲染方法中,看看是否适合直接保留在那里。)
- 努力将哪些状态可以舍弃。理想情况下,应该使用函数式组件;但是,要警惕“虚荣函数式组件”,即为了使其“函数式”而将本应在子组件中的状态保留在父组件中。这远比使用两个精心设计的有状态组件要糟糕得多。
- 将新组件合并到原组件中,替换标记。如果您发现自己直接向子组件传递了太多参数,那么您可能应该在第一步就停止,根本不应该将组件抽象出来。
何时将某个组件或例程抽象成独立的组件可能很难判断。有时,这纯粹是个人偏好;并没有绝对正确的方法。如有疑问,较小的组件更容易理解,但抽象应该有其目的。
你还想看到哪些重构模式的介绍?请留言告诉我!
文章来源:https://dev.to/jcutrell/react-anti-pattern-renderthing-50nd