五、制作可重复使用的组件

本章的重点是向您展示如何实现用于多种用途的 React 组件。阅读本章后,您将对如何编写应用功能充满信心。

在本章开始时,我们将简要介绍 HTML 元素,以及它们在帮助实现特性和使用高级别实用程序方面是如何工作的。然后,我们将研究单片组件的实现,并讨论它将导致的问题。下一节将致力于以这样一种方式重新实现单片组件,即该功能由更小的组件组成。

最后,本章最后讨论了 React 组件的渲染树,并提供了一些技巧,说明如何避免由于分解组件而引入太多的复杂性。我们将通过重申高级功能组件与实用组件的概念来结束这最后一节。

可重用的 HTML 元素

在开始使用 React 组件实现之前,让我们先考虑一下 HTML 元素。根据 HTML 元素的类型,它可以是以特性为中心的或以实用程序为中心的。以实用程序为中心的 HTML 元素比以功能为中心的 HTML 元素更易于重用。例如,考虑 AutoT0.元素。是的,这是一个可以在任何地方使用的通用元素,但其主要目的是组成特征的结构方面——特征的外壳和特征的内部部分。这就是<section>元素最有用的地方。

在围栏的另一边,我们有类似于<p><span><button>元素的东西。这些元素提供了高水平的实用性,因为它们在设计上是通用的。当我们有用户可以点击的东西时,我们应该使用<button>元素,从而产生一个动作。这是一个低于特性概念的级别。

虽然与面向特定功能的 HTML 元素相比,具有高实用性的 HTML 元素很容易讨论,但当涉及到数据时,讨论会更加详细。HTML 是静态标记组件,它将静态标记与数据相结合。问题是,我们如何做到这一点,并确保我们正在创建正确的以功能为中心和以实用程序为中心的组件?

本章其余部分的目的是了解如何从定义功能的单片 React 组件过渡到与实用程序组件相结合的更小的以功能为中心的组件。

单片组件的难度

如果您可以为任何给定的特性实现一个组件,那么事情就会非常简单,不是吗?至少,不会有太多的组件需要维护,也不会有太多的通信路径供数据通过,因为所有东西都是组件内部的。

然而,由于许多原因,这个想法不起作用。拥有单片特性组件使得协调任何类型的团队开发工作都变得困难。我注意到,对于单片组件,它们变得越大,以后重构成更好的组件就越困难。

还有特征重叠和特征交流的问题。重叠是由于功能之间的相似性而发生的。应用不太可能拥有一组彼此完全独特的功能。这将使应用很难学习和使用。组件通信本质上意味着一个特性中某个部件的状态将影响另一个特性中某个部件的状态。状态是很难处理的,当有很多状态打包到一个单片组件中时更是如此。

学习如何避免单片组件的最好方法是亲身体验。我们将在本节剩下的时间里实现一个单片组件。在下一节中,您将看到如何将该组件重构为更可持续的组件。

JSX 标记

我们要实现的单片组件是一个列出文章的特性。这只是为了说明的目的,所以我们不想在组件的大小上过火。这将是简单的,但整体。用户可以向列表中添加新项目、切换列表中项目的摘要以及从列表中删除项目。以下是组件的渲染方法:

render() { 
  const { 
    articles, 
    title, 
    summary, 
  } = this.data.toJS(); 

  return ( 
    <section> 
      <header> 
        <h1>Articles</h1> 
        <input 
          placeholder="Title" 
          value={title} 
          onChange={this.onChangeTitle} 
        /> 
        <input 
          placeholder="Summary" 
          value={summary} 
          onChange={this.onChangeSummary} 
        /> 
        <button onClick={this.onClickAdd}>Add</button> 
      </header> 
      <article> 
        <ul> 
          {articles.map(i => ( 
            <li key={i.id}> 
              <a 
                href="#" 

                onClick={this.onClickToggle.bind(null, i.id)} 
              > 
                {i.title} 
              </a> 
              &nbsp; 
              <a 
                href="#" 

                onClick={this.onClickRemove.bind(null, i.id)} 
              > 
                ✗
              </a> 
              <p style={{ display: i.display }}> 
                {i.summary} 
              </p> 
            </li> 
          ))} 
        </ul> 
      </article> 
    </section> 
  ); 
} 

所以,不是很多 JSX,但在一个地方肯定比必要的多。我们将在下一节对此进行改进,但现在,让我们实现此组件的初始状态。

我强烈建议您从下载本书的配套代码 https://github.com/PacktPublishing/React-and-React-Native 我可以分解组件代码,以便在这些页面上进行解释。但是,如果您除了运行代码模块之外,还可以看到整个代码模块,那么学习起来会更容易。

初始状态和状态帮助程序

现在让我们看看这个组件的初始状态:

// The state of this component is consists of 
// three properties: a collection of articles, 
// a title, and a summary. The "fromJS()" call 
// is used to build an "Immutable.js" Map. Also 
// note that this isn't set directly as the component 
// state - it's in a "data" property of the state - 
// otherwise, state updates won't work as expected. 
state = { 
  data: fromJS({ 
    articles: [ 
      { 
        id: cuid(), 
        title: 'Article 1', 
        summary: 'Article 1 Summary', 
        display: 'none', 
      }, 
      { 
        id: cuid(), 
        title: 'Article 2', 
        summary: 'Article 2 Summary', 
        display: 'none', 
      }, 
      { 
        id: cuid(), 
        title: 'Article 3', 
        summary: 'Article 3 Summary', 
        display: 'none', 
      }, 
      { 
        id: cuid(), 
        title: 'Article 4', 
        summary: 'Article 4 Summary', 
        display: 'none', 
      }, 
    ], 
    title: '', 
    summary: '', 
  }), 
} 

国家本身没有什么特别之处;它只是一个对象的集合。有两个有趣的函数用于初始化状态。第一个是来自cuid包的cuid(),这是一个生成唯一 ID 的有用工具。第二个是immutable套餐中的fromJS()。以下是引入这两个依赖项的导入:

// Utility for constructing unique IDs... 
import cuid from 'cuid'; 

// For building immutable component states... 
import { fromJS } from 'immutable'; 

顾名思义,fromJS()函数用于构造不可变的数据结构。Immutable.js具有非常有用的功能,用于操纵 React 组件的状态。我们将在本书的其余部分使用Immutable.js,从这个例子开始,您将了解更多的细节。

您可能还记得上一章中提到的setState()方法仅适用于普通对象。嗯,Immutable.js对象不是普通对象。如果我们想使用不可变数据,我们需要以某种方式将它们包装在一个普通对象中。让我们为此实现一个 helper getter 和 setter:

// Getter for "Immutable.js" state data... 
get data() { 
  return this.state.data; 
} 

// Setter for "Immutable.js" state data... 
set data(data) { 
  this.setState({ data }); 
} 

现在,我们可以在事件处理程序中使用不可变组件状态。

事件处理程序实现

此时,我们有了组件的初始状态、状态助手属性和 JSX。现在是实现事件处理程序本身的时候了:

// When the title of a new article changes, update the state 
// of the component with the new title value, by using "set()" 
// to create a new map. 
onChangeTitle = (e) => { 
  this.data = this.data.set( 
    'title', 
    e.target.value, 
  ); 
} 

// When the summary of a new article changes, update the state 
// of the component with the new summary value, by using "set()" 
// to create a new map. 
onChangeSummary = (e) => { 
  this.data = this.data.set( 
    'summary', 
    e.target.value 
  ); 
} 

// Creates a new article and empties the title 
// and summary inputs. The "push()" method creates a new 
// list and "update()" is used to update the list by 
// creating a new map. 
onClickAdd = () => { 
  this.data = this.data 
    .update( 
      'articles', 
      a => a.push(fromJS({ 
        id: cuid(), 
        title: this.data.get('title'), 
        summary: this.data.get('summary'), 
        display: 'none', 
      })) 
    ) 
    .set('title', '') 
    .set('summary', ''); 
} 

// Removes an article from the list. Calling "delete()" 
// creates a new list, and this is set in the new component 
// state. 
onClickRemove = (id) => { 
  const index = this.data 
    .get('articles') 
    .findIndex( 
      a => a.get('id') === id 
    ); 

  this.data = this.data 
    .update( 
      'articles', 
      a => a.delete(index) 
    ); 
} 

// Toggles the visibility of the article summary by 
// setting the "display" state of the article. This 
// state is dependent on the current state. 
onClickToggle = (id) => { 
  const index = this.data 
    .get('articles') 
    .findIndex( 
      a => a.get('id') === id 
    ); 

  this.data = this.data 
    .update( 
      'articles', 
      articles => articles.update( 
        index, 
        a => a.set( 
          'display', 
          a.get('display') ? '' : 'none' 
        ) 
      ) 
    ); 
}

哎呀!那是很多代码!不用担心,它实际上非常简单,特别是与使用普通 JavaScript 实现这些转换相比。以下是一些帮助您理解此代码的指针:

  • setState()总是以普通对象作为参数调用。这就是我们引入数据设置器的原因。当你给this.data赋值时,它会用一个普通对象调用setState()。你只需要担心Immutable.js数据。同样,数据获取程序返回Immutable.js对象,而不是整个状态。
  • 不可变方法总是返回一个新实例。当你看到像article.set(...)这样的东西时,它实际上并没有改变article,而是创建了一个新的。
  • render()方法中,不可变的数据结构被转换回普通 JavaScript 数组和对象,以便在 JSX 标记中使用。

如果有必要,花所有你需要的时间去了解这里发生了什么。在阅读本书的过程中,您将看到 React 组件可以利用不可变状态的方法。这里值得指出的另一点是,这些事件处理程序只能更改此组件的状态。也就是说,它们不会意外更改其他组件的状态。正如您将在下一节中看到的,这些处理程序实际上状态非常好。

以下是渲染输出的屏幕截图:

Event handler implementation

重构组件结构

我们现在有一个单一的功能组件了什么?让我们做得更好。

在本节中,您将学习如何使用我们在上一节中刚刚实现的功能组件,并将其拆分为更易于维护的组件。我们将从 JSX 开始,因为这可能是最好的重构起点。然后,我们将为该特性实现新组件。

最后,我们将使这些新组件功能化,而不是基于类。

从 JSX 开始

任何单片组件的 JSX 都是了解如何将其重构为更小组件的最佳起点。让我们设想一下当前正在重构的组件的结构:

Start with the JSX

JSX 的顶部是表单控件,因此它很容易成为自己的组件:

<header> 
  <h1>Articles</h1> 
  <input 
    placeholder="Title" 
    value={title} 
    onChange={this.onChangeTitle} 
  /> 
  <input 
    placeholder="Summary" 
    value={summary} 
    onChange={this.onChangeSummary} 
  /> 
  <button onClick={this.onClickAdd}>Add</button> 
</header> 

接下来,我们有文章列表:

<ul> 
  {articles.map(i => ( 
    <li key={i.id}> 
      <a 
        href="#" 

        onClick={ 
          this.onClickToggle.bind(null, i.id) 
        } 
      > 
        {i.title} 
      </a> 
      &nbsp; 
      <a 
        href="#" 

        onClick={this.onClickRemove.bind(null, i.id)} 
      > 
        ✗
      </a> 
      <p style={{ display: i.display }}> 
        {i.summary} 
      </p> 
    </li> 
  ))} 
</ul> 

在这个列表中,我们有一个潜在的物品,它是<li>标签中的所有物品。

如您所见,仅 JSX 就描绘了如何将 UI 结构分解为更小的组件。如果没有声明性 JSX 标记,这种重构练习将很困难。

实现物品列表组件

下面是文章列表组件实现的样子:

import React, { Component } from 'react'; 

export default class ArticleList extends Component { 
  render() { 
    // The properties include things that are passed in 
    // from the feature component. This includes the list 
    // of articles to render, and the two event handlers 
    // that change state of the feature component. 
    const { 
      articles, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 

    return ( 
      <ul> 
        {articles.map(i => ( 
          <li key={i.id}> 
            { /* The "onClickToggle()" callback changes 
                 the state of the "MyFeature" component. */ } 
            <a 
              href="#" 

              onClick={onClickToggle.bind(null, i.id)} 
            > 
              {i.title} 
            </a> 
            &nbsp; 

            { /* The "onClickRemove()" callback changes 
                 the state of the "MyFeature" component. */ } 
            <a 
              href="#" 

              onClick={onClickRemove.bind(null, i.id)} 
            > 
              ✗ 
            </a> 
            <p style={{ display: i.display }}> 
              {i.summary} 
            </p> 
          </li> 
        ))} 
      </ul> 
    ); 
  } 
} 

正如您所看到的,我们只是从单片组件中取出相关的 JSX 并将其放在这里。现在让我们看看功能组件 JSX 是什么样子:

render() { 
  const { 
    articles, 
    title, 
    summary, 
  } = this.data.toJS(); 

  return ( 
    <section> 
      <header> 
        <h1>Articles</h1> 
        <input 
          placeholder="Title" 
          value={title} 
          onChange={this.onChangeTitle} 
        /> 
        <input 
          placeholder="Summary" 
          value={summary} 
          onChange={this.onChangeSummary} 
        /> 
        <button onClick={this.onClickAdd}>Add</button> 
      </header> 

      { /* Now the list of articles is rendered by the 
           "ArticleList" component. This component can 
           now be used in several other components. */ } 
      <ArticleList 
        articles={articles} 
        onClickToggle={this.onClickToggle} 
        onClickRemove={this.onClickRemove} 
      /> 
    </section> 
  ); 
} 

文章列表现在由<ArticleList>组件呈现。要呈现的项目列表作为属性以及两个事件处理程序传递给此组件。

等等,为什么我们要将事件处理程序传递给子组件?原因很简单,;这样ArticleList组件就不必担心状态或状态如何变化。它所关心的只是呈现内容,并确保适当的事件回调连接到适当的 DOM 元素。这是一个容器组件的概念,我将在本章后面的部分详细介绍。

实现物品项目组件

在实现了 article list 组件之后,您可能会认为进一步分解该组件是一个好主意,因为该项目可能会呈现在另一个页面上的另一个列表中。也许,将项目列表项实现为其自己的组件最重要的方面是,我们不知道标记将来会如何更改。

另一种看待它的方式是,如果事实证明我们实际上不需要该项作为它自己的组件,那么这个新组件不会引入太多间接性或复杂性。不用多说,下面是文章项组件:

import React, { Component } from 'react'; 

export default class ArticleItem extends Component { 
  render() { 
    // The "article" is mapped from the "ArticleList" 
    // component. The "onClickToggle()" and 
    // "onClickRemove()" event handlers are passed 
    // all the way down from the "MyFeature" component. 
    const { 
      article, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 

    return ( 
      <li> 
        { /* The "onClickToggle()" callback changes 
             the state of the "MyFeature" component. */ } 
        <a 
          href="#" 

          onClick={onClickToggle.bind(null, article.id)} 
        > 
          {article.title} 
        </a> 
        &nbsp; 

        { /* The "onClickRemove()" callback changes 
             the state of the "MyFeature" component. */ } 
        <a 
          href="#" 

          onClick={onClickRemove.bind(null, article.id)} 
        > 
          ✗ 
        </a> 
        <p style={{ display: article.display }}> 
          {article.summary} 
        </p> 
      </li> 
    ); 
  } 
} 

下面是由ArticleList组件呈现的新ArticleItem组件:

import React, { Component } from 'react'; 
import ArticleItem from './ArticleItem'; 

export default class ArticleList extends Component { 
  render() { 
    // The properties include things that are passed in 
    // from the feature component. This includes the list 
    // of articles to render, and the two event handlers 
    // that change state of the feature component. These, 
    // in turn, are passed to the "ArticleItem" component. 
    const { 
      articles, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 

    // Now this component maps to an "<ArticleItem>"  
    // collection. 
    return ( 
      <ul> 
        {articles.map(i => ( 
          <ArticleItem 
            key={i.id} 
            article={i} 
            onClickToggle={onClickToggle} 
            onClickRemove={onClickRemove} 
          /> 
        ))} 
      </ul> 
    ); 
  } 
} 

你看到这个列表如何映射文章列表了吗?如果我们想实现另一个文章列表,它也会进行一些过滤,那该怎么办?拥有一个可重用的ArticleItem组件是有益的。

实现添加物品组件

现在我们已经完成了文章列表,是时候考虑一下用于添加新文章的表单控件了。让我们为功能的这一方面实现一个组件:

import React, { Component } from 'react'; 

export default class AddArticle extends Component{ 
  render() { 
    const { 
      name, 
      title, 
      summary, 
      onChangeTitle, 
      onChangeSummary, 
      onClickAdd 
    } = this.props; 

    return ( 
      <section> 
        <h1>{name}</h1> 
        <input 
          placeholder="Title" 
          value={title} 
          onChange={onChangeTitle} 
        /> 
        <input 
          placeholder="Summary" 
          value={summary} 
          onChange={onChangeSummary} 
        /> 
        <button onClick={onClickAdd}>Add</button> 
      </section> 
    ); 
  } 
} 

现在,我们有了功能组件 JSX 的最终版本:

render() { 
  const {  
    articles,  
    title,  
    summary, 
  } = this.state.data.toJS(); 

  return ( 
    <section> 
      { /* Now the add article form is rendered by the 
           "AddArticle" component. This component can 
           now be used in several other components. */ } 
      <AddArticle 
        name="Articles" 
        title={title} 
        summary={summary} 
        onChangeTitle={this.onChangeTitle} 
        onChangeSummary={this.onChangeSummary} 
        onClickAdd={this.onClickAdd} 
      /> 

      { /* Now the list of articles is rendered by the 
           "ArticleList" component. This component can 
           now be used in several other components. */ } 
      <ArticleList 
        articles={articles} 
        onClickToggle={this.onClickToggle} 
        onClickRemove={this.onClickRemove} 
      /> 
    </section> 
  ); 
} 

正如您所看到的,该组件的重点是功能数据,而呈现 UI 元素则依赖于其他组件。让我们对我们为这个特性实现的新组件做最后的调整。

使组件功能化

在为该特性实现这些新组件时,您可能已经注意到,除了使用属性值呈现 JSX 之外,它们没有任何责任。这些组件是纯功能组件的良好候选组件。每当您遇到只使用属性值的组件时,最好让它们正常工作。首先,它明确了组件不依赖于任何状态或生命周期方法。它也更高效,因为当 React 检测到组件是函数时,它不会执行那么多的工作。

以下是文章列表组件的功能版本:

import React from 'react'; 
import ArticleItem from './ArticleItem'; 

export default ({ 
  articles, 
  onClickToggle, 
  onClickRemove, 
}) => ( 
  <ul> 
    {articles.map(i => ( 
      <ArticleItem 
        key={i.id} 
        article={i} 
        onClickToggle={onClickToggle} 
        onClickRemove={onClickRemove} 
      /> 
    ))} 
  </ul> 
); 

以下是 article item 组件的功能版本:

import React from 'react'; 

export default ({ 
  article, 
  onClickToggle, 
  onClickRemove, 
}) => ( 
  <li> 
    { /* The "onClickToggle()" callback changes 
         the state of the "MyFeature" component. */ } 
    <a 
      href="#" 

      onClick={onClickToggle.bind(null, article.id)} 
    > 
      {article.title} 
    </a> 
    &nbsp; 

    { /* The "onClickRemove()" callback changes 
         the state of the "MyFeature" component. */ } 
    <a 
      href="#" 

      onClick={onClickRemove.bind(null, article.id)} 
    > 
      ✗ 
    </a> 
    <p style={{ display: article.display }}> 
      {article.summary} 
    </p> 
  </li> 
); 

以下是 add article 组件的功能版本:

import React from 'react'; 

export default ({ 
  name, 
  title, 
  summary, 
  onChangeTitle, 
  onChangeSummary, 
  onClickAdd, 
}) => ( 
  <section> 
    <h1>{name}</h1> 
    <input 
      placeholder="Title" 
      value={title} 
      onChange={onChangeTitle} 
    /> 
    <input 
      placeholder="Summary" 
      value={summary} 
      onChange={onChangeSummary} 
    /> 
    <button onClick={onClickAdd}>Add</button> 
  </section> 
); 

使组件功能化的另一个好处是,引入不必要的方法或其他数据的机会较少,因为它不是一个更容易添加更多内容的类。

绘制组件树

在上一节中,我们将一个大型单片组件重构为几个更小的、更关注组件的组件。让我们花一点时间思考一下我们所取得的成就。曾经是单片的特性组件最终几乎完全集中在状态数据上。它处理初始状态和状态转换,并处理获取状态的网络请求(如果有的话)。这是 React 应用中典型的容器组件,它是数据的起点。

为了更好地组合功能,我们实现的新组件就是这些数据的接收者。这些组件及其容器之间的区别在于,它们只关心在渲染时传递给它们的属性。换句话说,他们只关心特定时间点的数据快照。从这里,这些组件可以将属性数据作为属性传递到它们自己的子组件中。组成 React 组件的通用模式如下所示:

Rendering component trees

容器组件通常包含一个直接子级。在该图中,您可以看到容器具有 item detail 组件或 list 组件。当然,这两个类别会有所不同,因为每个应用都是不同的。此通用模式有三个组件组成级别。数据沿着一个方向从容器一直向下流动到实用程序组件。

一旦添加了三个以上的层,应用体系结构就会变得难以理解。有一种奇怪的情况,您需要添加四层 React 组件,但根据经验,您应该避免这样做。

功能部件和实用部件

正如您在单片组件示例中看到的,我们从一个完全关注某个特性的单个组件开始。这意味着该组件在应用的其他地方几乎没有什么用处。

这是因为顶级组件处理应用状态。有状态组件很难在任何其他上下文中使用。在重构单片特征组件时,您创建了新的组件,这些组件远离数据。一般的规则是,组件离有状态数据越远,它们的实用性就越强,因为它们的属性值可以从应用中的任何位置传入。

总结

本章是关于避免单片组件设计的。然而,在任何 React 组件的设计中,整块石通常是一个必要的起点。

我们首先讨论 HTML 以及不同元素如何具有不同程度的实用性。接下来,我们讨论了单片 React 组件的问题,并介绍了单片组件的实现。

然后,您花了几节时间学习如何将单片组件重构为更可持续的设计。从本练习中,您了解到容器组件应该只考虑处理状态,而较小的组件具有更大的实用性,因为它们的属性值可以从任何地方传递。

在下一章中,您将了解 React 组件生命周期。对于实现容器组件,这是一个特别相关的主题。