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

React 反模式:renderThing DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

它的用法如下:

<Tabs tabs={[{title: "Tab One", id: "tab-one"}, {title: "Tab Two", id: "tab-two"}]} />
Enter fullscreen mode Exit fullscreen mode

这样就行了!如果这就是你对这个组件的全部需求,那么完全可以到此为止了!

但如果这段代码将来会发生变化,你最终可能会得到一个复杂冗长的组件。

这里最明显也是最先出现的重构问题在于这个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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

这本身其实是一个非常好的组件。但将来你可能需要在其他地方使用同样的标签式按钮,所以我们来看看能不能让这个按钮可以共享。

我们单独来看一个标签页。

<a onClick={e => this.setActiveTab(tab.id)}
   key={tab.id}
   className={this.state.activeTab == tab.id ? "active" : ""}
>
  {tab.title}
</a>
Enter fullscreen mode Exit fullscreen mode

现在让我们把这个组件做成一个独立的函数式组件。(换句话说,我们希望这个组件接收 props,但不需要它拥有自己的状态。)

const TabButton = ({ onClick, active, title, tabId, ...props}) => (
  <a onClick={e => {e.preventDefault(); props.onClick(tabId)}}
    {...props}
    className={active ? "active" : ""}
  >
    {title}
  </a>
)
Enter fullscreen mode Exit fullscreen mode

现在我们有了一个功能组件,我们可以将其集成回我们原来的 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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

那么我们究竟能从中获得什么好处呢?

  • 移除了不必要/令人困惑的 renderTabs 按钮
  • 创建了一个可重用的 TabButton 组件,它不依赖任何外部状态。
  • Tabs接口API 没有变化
  • 两个较小的组件比一个较大的组件更容易进行推理和分离关注点。

这个例子虽然人为设计且规模较小,但你几乎肯定会找到renderThing怪物出没的地方。

重构模式如下所示:

  1. 将怪物renderThing方法的代码移回原始渲染器中即可。如果代码合理,到此为止即可。
  2. 从渲染输出中提取一部分作为新组件。(请注意,您可以直接跳到此步骤,跳过步骤 1,但我喜欢先将其移回渲染方法中,看看是否适合直接保留在那里。)
  3. 努力将哪些状态可以舍弃。理想情况下,应该使用函数式组件;但是,要警惕“虚荣函数式组件”,即为了使其“函数式”而将本应在子组件中的状态保留在父组件中。这远比使用两个精心设计的有状态组件要糟糕得多。
  4. 将新组件合并到原组件中,替换标记。如果您发现自己直接向子组件传递了太多参数,那么您可能应该在第一步就停止,根本不应该将组件抽象出来。

何时将某个组件或例程抽象成独立的组件可能很难判断。有时,这纯粹是个人偏好;并没有绝对正确的方法。如有疑问,较小的组件更容易理解,但抽象应该有其目的。

你还想看到哪些重构模式的介绍?请留言告诉我!

文章来源:https://dev.to/jcutrell/react-anti-pattern-renderthing-50nd