(译)React 组件设计模式基础

4,313

原文链接:React Component Patterns

作者:Gustavo Matheus

随着 React 在前端开发中越来越流行,各种各样的设计模式及新概念亦层出不穷。本文旨在总结 React 开发中一些常见的设计模式。

有状态 (Stateful) vs 无状态 (stateless)

React 组件可以是有状态的,在其生命周期内可以操纵并改变其内部状态;React 组件也可以是无状态的,它仅接受来自父组件传入的 props,并进行展示。

下面是一个无状态Button 组件,它的行为完全由传入的 props 决定:

const Button = props => 
  <button onClick={props.onClick}>
    {props.text}
  </button>

下面是一个有状态组件(使用了上述的无状态组件):

class ButtonCounter extends React.Component {
  constructor() {
    super();
    this.state = { clicks: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({ clicks: ++this.state.clicks });
  }

  render() {
    return (
      <Button
        onClick={this.handleClick}
        text={`You've clicked me ${this.state.clicks} times !`}
      />
    )
  }
}

正如你所看到的,上述 ButtonCounter 组件在 state 中维护了自己的状态,而之前的 Button 组件仅根据 props 来进行渲染展示。这个区别看似很小,但是无状态的 Button 组件却高度可复用。

容器(Container) vs 展示(Presentational) 组件

当与外部数据进行交互时,我们可以把组件分为两类:

  • 容器组件:主要负责同外部数据进行交互(通信),譬如与 Redux 等进行数据绑定等。
  • 展示组件:根据自身 state 及接收自父组件的 props 做渲染,并不直接与外部数据源进行沟通。

我们来看一个展示组件:

const UserList = props =>
  <ul>
    {props.users.map(u => (
      <li>{u.name} - {u.age} years old</li>
    ))}
  </ul>

而这个展示组件可以被一个容器组件更新:

class UserListContainer extends React.Component {
  constructor() {
    super()
    this.state = { users: [] }
  }

  componentDidMount() {
    fetchUsers(users => this.setState({ users }));
  }

  render() {
    return <UserList users={this.state.users} />
  }
}

通过将组件区分为容器组件与展示组件,将数据获取与渲染进行分离。这也使 UserList 可复用。如果你想了解更多,这里有一些非常好的文章,解释地非常清楚。

高阶(Higher-Order)组件

当你想复用一个组件的逻辑时,高阶组件(HOC)就派上用场了。高阶组件就是 JavaScript 函数,接收 React 组件作为参数,并返回一个新组件。

举个例子:编写一个菜单组件,当点击一个菜单项时,展开当前菜单项,显示子菜单。当然我们可以在父组件里来控制此菜单组件的状态,但是更优雅的方式,是使用高阶组件:

function makeToggleable(Clickable) {
  return class extends React.Component {
    constructor() {
      super();
      this.toggle = this.toggle.bind(this);
      this.state = { show: false };
    }

    toggle() {
      this.setState({ show: !this.state.show });
    }

    render() {
      return (
        <div>
          <Clickable
            {...this.props}
            onClick={this.toggle}
          />
          {this.state.show && this.props.children}
        </div>
      );
    }
  }
}

通过这种方式,我们可以使用 JavaScript 的装饰器语法,将我们的逻辑应用于 ToggleableMenu 组件:

@makeToggleable
class ToggleableMenu extends React.Component {
  render() {
    return (
      <div onClick={this.props.onClick}>
        <h1>{this.props.title}</h1>
      </div>
    );
  }
}

现在,我们可以将任何子菜单内容放入 ToggleableMenu 组件中:

class Menu extends React.Component {
  render() {
    return (
      <div>
        <ToggleableMenu title="First Menu">
          <p>Some content</p>
        </ToggleableMenu>
        <ToggleableMenu title="Second Menu">
          <p>Another content</p>
        </ToggleableMenu>
        <ToggleableMenu title="Third Menu">
          <p>More content</p>
        </ToggleableMenu>
      </div>
    );
  }
}

当你在使用 Reduxconnect,或者 React RouterwithRouter 函数时,你就是在使用高阶组件!

渲染回调(Render Callbacks)

除了上述的高阶组件外,渲染回调是另一种使组件可复用的设计模式。渲染回调的核心是组件接收的子组件(或子结点,亦即 props.children),不以 React Component 提供,而是以回调函数的形式提供。以上述 HOC 组件为例,我们通过渲染回调的方式重写如下:

class Toggleable extends React.Component {
  constructor() {
    super();
    this.toggle = this.toggle.bind(this);
    this.state = { show: false }
  }

  toggle() {
    this.setState({ show: !this.state.show });
  }

  render() {
    return this.props.children(this.state.show, this.toggle)
  }
}

现在,我们可以传入回调函数给 Toggleable 组件作为子结点。 我们用新方式实现之前的 HOC 组件 ToggleableMenu

const ToggleableMenu = props => (
  <Toggleable>
    {(show, onClick) => (
      <div>
        <div onClick={onClick}>
          <h1>{props.title}</h1>
        </div>
        { show && props.children }
      </div>
    )}
  </Toggleable>
)

而我们全新的 Menu 组件实现如下:

class Menu extends React.Component {
  render() {
    return (
      <div>
        <ToggleableMenu title="First Menu">
          <p>Some content</p>
        </ToggleableMenu>
        <ToggleableMenu title="Second Menu">
          <p>Another content</p>
        </ToggleableMenu>
        <ToggleableMenu title="Third Menu">
          <p>More content</p>
        </ToggleableMenu>
      </div>
    );
  }
}

是的,你没有看错,新的 Menu 组件同之前以HOC模式实现出来的一模一样!

在这种实现方式下,我们将组件内部的状态(state)与组件的渲染逻辑进行剥离。在上面的例子中,我们将渲染逻辑放在了 ToggleableMenu渲染回调中,而展示组件的状态(state)依然在 Toggleable 组件内进行维护。

了解更多

以上的一些例子仅仅是 React 设计模式的基础知识。如果你想更加深入地了解关于 React 设计模式的话题,以下是一些非常好的学习资料,值得一看:


关注微信公众号:创宇前端(KnownsecFED),码上获取更多优质干货!