八、扩展组件

在本章中,您将学习如何通过扩展现有组件向其添加新功能。有两种 React 机制可用于扩展组件,我们将依次介绍它们:

  • 组件继承
  • 具有高阶分量的合成

我们将从基本组件继承开始,就像您习惯的良好的旧面向对象类层次结构一样。然后,我们将实现一些高阶组件,它们用作 React 组件组成中的组件。

组件继承

组件只是类。事实上,当您使用ES2015类语法实现组件时,您从 React 扩展了基础 Component类。您可以继续像这样扩展类来创建自己的基本组件。

在本节中,您将看到组件如何继承状态、属性和其他任何内容,包括 JSX 标记和事件处理程序。

继承状态

有时,您有几个使用相同初始状态的 React 组件。可以实现设置此初始状态的基本组件。然后,任何想要将其用作初始状态的组件都可以扩展该组件。让我们实现一个设置一些基本状态的基本组件:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

export default class BaseComponent extends Component { 
  state = { 
    data: fromJS({ 
      name: 'Mark', 
      enabled: false, 
      placeholder: '', 
    }), 
  } 

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

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

  // The base component doesn't actually render anything, 
  // but it still needs a render method. 
  render() { 
    return null; 
  } 
} 

如您所见,状态是一个不可变的Map。这个基本组件还实现了不可变的数据 setter 和 getter 方法。让我们实现一个扩展此组件的组件:

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

// Extends "BaseComponent" to inherit the 
// initial component state. 
export default class MyComponent extends BaseComponent { 

  // This is our chance to build on the initial state. 
  // We change the "placeholder" text and mark it as 
  // "enabled". 
  componentWillMount() { 
    this.data = this.data 
      .merge({ 
        placeholder: 'Enter a name...', 
        enabled: true, 
      }); 
  } 

  // Renders a simple input element, that uses the 
  // state of this component as properties. 
  render() { 
    const { 
      enabled, 
      name, 
      placeholder, 
    } = this.data.toJS(); 

    return ( 
      <label htmlFor="my-input"> 
        Name: 
        <input 
          id="my-input" 
          disabled={!enabled} 
          defaultValue={name} 
          placeholder={placeholder} 
        /> 
      </label> 
    ); 
  } 
} 

该组件实际上不必设置任何初始状态,因为它已经由BaseComponent设置。由于状态已经是不可变的Map,我们可以使用merge()componentWillMount()中调整初始状态。以下是渲染输出的外观:

Inheriting state

如果删除输入元素中的默认文本,可以看到MyComponent添加到初始状态的占位符文本按预期应用:

Inheriting state

继承属性

继承属性的工作方式正是您所期望的。将默认特性值和特性类型定义为基类中的静态特性。继承此基础的任何类也会继承特性值和特性规范。让我们来看一个基类实现:

import React, { Component, PropTypes } from 'react'; 

export default class BaseComponent extends Component { 
  // The specification for these base properties. 
  static propTypes = { 
    users: PropTypes.array.isRequired, 
    groups: PropTypes.array.isRequired, 
  } 

  // The default values of these base properties. 
  static defaultProps = { 
    users: [], 
    groups: [], 
  } 

  render() { 
    return null; 
  } 
} 

这个类本身实际上什么都不做。我们定义它的唯一原因是为了有一个地方可以声明默认属性值及其类型约束。分别是defaultPropspropTypes静态类属性。

现在,让我们来看一个继承这些属性的组件:

import React from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 

import BaseComponent from './BaseComponent'; 

// Renders the given "text" as a header, unless 
// the given "length" is 0\. 
const SectionHeader = ({ text, length }) => 
  ImmutableMap() 
    .set(0, null) 
    .get(length, (<h1>{text}</h1>)); 

export default class MyComponent extends BaseComponent { 
  render() { 
    const { users, groups } = this.props; 

    // Renders the "users" and "groups" arrays. There 
    // are not property validators or default values 
    // in this component, since these are declared in 
    // "BaseComponent". 
    return ( 
      <section> 
        <SectionHeader 
          text="Users" 
          length={users.length} 
        /> 
        <ul> 
          {users.map(i => ( 
            <li key={i}>{i}</li> 
          ))} 
        </ul> 

        <SectionHeader 
          text="Groups" 
          length={groups.length} 
        /> 
        <ul> 
          {groups.map(i => ( 
            <li key={i}>{i}</li> 
          ))} 
        </ul> 
      </section> 
    ); 
  } 
} 

让我们尝试渲染MyComponent以确保继承的属性按预期工作:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyComponent from './MyComponent'; 

const users = [ 
  'User 1', 
  'User 2', 
]; 

const groups = [ 
  'Group 1', 
  'Group 2', 
]; 

render(( 
  <section> 
    { /* Renders as expected, using the defaults. */ } 
    <MyComponent /> 

    { /* Renders as expected, using the "groups" default. */ } 
    <MyComponent users={users} /> 
    <hr /> 

    { /* Renders as expected, using the "users" default. */ } 
    <MyComponent groups={groups} /> 
    <hr /> 

    { /* Renders as expected, providing property values. */ } 
    <MyComponent users={users} groups={groups} /> 

    { /* Fails to render, the property validators in the base 
         component detect the invalid number type. */ } 
    <MyComponent users={0} groups={0} /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

如您所见,尽管MyComponent没有定义任何属性默认值或类型,但我们得到了预期的行为。当我们试图将数字传递给usersgroups属性时,我们看不到任何渲染。这是因为MyComponent希望对这些属性值使用map()方法,但没有。但是,在发生此异常之前,您可以看到属性验证失败警告,它准确地解释了发生的情况。在本例中,我们传递了一个意外的类型。

如果我们删除最后一个元素,其他所有元素都会呈现良好效果。以下是渲染内容的外观:

Inheriting properties

继承 JSX 和事件处理程序

关于 React 组件继承,我们将涉及的最后一个领域是 JSX 和事件处理程序。如果您有一个具有相同 UI 元素和事件处理逻辑的单一 UI 组件,但根据组件的使用位置,初始状态应该是什么存在差异,那么您可能希望采用这种方法。

例如,基类将定义 JSX 和事件处理程序方法,而更具体的组件将定义该特性特有的初始状态。下面是一个示例基类:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

export default class BaseComponent extends Component { 
  state = { 
    data: fromJS({ 
      items: [], 
    }), 
  } 

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

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

  // The click event handler for each item in the 
  // list. The context is the lexically-bound to 
  // this component. 
  onClick = id => () => { 
    this.data = this.data 
      .update( 
        'items', 
        items => items 
          .update( 
            items.indexOf(items.find(i => i.get('id') === id)), 
            item => item.update('done', d => !d) 
          ) 
      ); 
  }; 

  // Renders a list of items based on the state 
  // of the component. The style of the item 
  // depends on the "done" property of the item. 
  // Each item is assigned an event handler that 
  // toggles the "done" state. 
  render() { 
    const { items } = this.data.toJS(); 

    return ( 
      <ul> 
        {items.map(i => ( 
          <li 
            key={i.id} 
            onClick={this.onClick(i.id)} 
            style={{ 
              cursor: 'pointer', 
              textDecoration: i.done ? 
                'line-through' : 'none', 
            }} 
          >{i.name}</li> 
        ))} 
      </ul> 
    ); 
  } 
} 

此基本组件呈现一个项目列表,单击该列表时,可切换项目文本的样式。默认情况下,此组件的状态具有空项列表。这意味着在不设置组件状态的情况下渲染此组件是安全的。但是,这不是很有用,因此让我们通过继承基本组件并设置状态来为该列表提供一些项:

import React from 'react'; 
import { fromJS } from 'immutable'; 
import BaseComponent from './BaseComponent'; 

export default class MyComponent extends BaseComponent { 

  // Initializes the component state, by using the 
  // "data" getter method from "BaseComponent". 
  componentWillMount() { 
    this.data = this.data 
      .merge({ 
        items: [ 
          { id: 1, name: 'One', done: false }, 
          { id: 2, name: 'Two', done: false }, 
          { id: 3, name: 'Three', done: false }, 
        ], 
      }); 
  } 
} 

记住,componentWillMount()生命周期方法可以安全地设置组件的状态。在这里,基本组件使用我们的datasetter/getter 来更改组件的状态。这种方法的另一个方便之处是,如果我们想要覆盖基本组件的一个事件处理程序,那么很容易做到:只需在MyComponent中定义该方法。

下面是列表呈现时的外观:

Inheriting JSX and event handlers

以下是单击所有项目后列表的外观:

Inheriting JSX and event handlers

具有高阶成分的成分

在本章的最后一节中,我们将介绍高阶组件。如果您熟悉函数式编程中的高阶函数,那么高阶组件的工作方式也是一样的。高阶函数是一个以另一个函数作为输入,并返回一个新函数作为输出的函数。此返回函数以某种方式调用原始函数。这个想法是在现有行为的基础上构建新的行为。

对于高阶 React 组件,您有一个函数,该函数将一个组件作为输入,并返回一个新组件作为输出。这是在 React 应用中编写新行为的首选方法,而且许多流行的 React 库似乎正在朝着这个方向发展,如果它们还没有这样做的话。以这种方式组合功能时,会有更大的灵活性。

条件组件渲染

高阶组件的一个明显用例是条件渲染。例如,根据某个谓词的结果,呈现组件或不呈现任何内容。谓词可以是特定于应用的任何内容,例如权限或类似的内容。

在 React 中实现类似的东西非常简单。假设我们有一个超级简单的组件:

import React from 'react'; 

// The world's simplest component... 
export default () => ( 
  <p>My component...</p> 
); 

现在,为了控制这个组件的显示,我们将用另一个组件包装它。包装由高阶函数处理。

如果您在 React 上下文中听到术语包装器,那么它可能指的是一个高阶组件。本质上,这就是它所做的,它包装传递给它的组件。

现在让我们看看创建高阶 React 组件有多容易:

import React from 'react'; 

// A minimal higher-order function is all it 
// takes to create a component repeater. Here, we're 
// returning a function that calls "predicate()". 
// If this returns true, then the rendered 
// "<Component>" is returned. 
export default (Component, predicate) => 
  props => 
    predicate() && (<Component {...props} />); 

只有三行吗?你在开玩笑吧?这都要感谢我们返回了一个功能组件。这个函数的两个参数是Component,这是我们正在包装的组件,以及要调用的predicate。如您所见,如果对predicate()的调用返回 true,则返回<Component>。否则,将不会呈现任何内容。

现在,让我们实际使用此函数组合一个新组件,以及呈现一段文本的超级简单组件:

import React from 'react'; 
import { render } from 'react-dom'; 

import cond from './cond'; 
import MyComponent from './MyComponent'; 

// Two compositions of "MyComponent". The 
// "ComposedVisible" version will render 
// because the predicate returns true. The 
// "ComposedHidden" version doesn't render. 
const ComposedVisible = cond(MyComponent, () => true); 
const ComposedHidden = cond(MyComponent, () => false); 

render(( 
  <section> 
    <h1>Visible</h1> 
    <ComposedVisible /> 
    <h2>Hidden</h2> 
    <ComposedHidden /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

我们刚刚使用MyComponentcond()predicate函数创建了两个新组件。如果你问我的话,这很强大。以下是渲染输出:

Conditional component rendering

提供数据源

让我们通过查看更复杂的高阶组件示例来完成本章。您将实现一个数据存储函数,用数据源包装给定组件。这种类型的模式很容易理解,因为Redux等 React 库都使用它。下面是用来包装组件的connect()函数:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

// The components that are connected to this store. 
let components = fromJS([]); 

// The state store itself, where application data is kept. 
let store = fromJS({}); 

// Sets the state of the store, then sets the 
// state of every connected component. 
export function setState(state) { 
  store = state; 

  for (const component of components) { 
    component.setState({ 
      data: store, 
    }); 
  } 
} 

// Returns the state of the store. 
export function getState() { 
  return store; 
} 

// Returns a higher-order component that's connected 
// to the "store". 
export function connect(ComposedComponent) { 
  return class ConnectedComponent extends Component { 

    state = { data: store } 

    // When the component is mounted, add it to 
    // "components", so that it will receive updates  
    // when the store state changes. 
    componentWillMount() { 
      components = components.push(this); 
    } 

    // Deletes this component from "components" when it is 
    // unmounted from the DOM. 
    componentWillUnmount() { 
      const index = components.findIndex(this); 
      components = components.delete(index); 
    } 

    // Renders "ComposedComponent", using the "store" state 
    // as properties. 
    render() { 
      return (<ComposedComponent {...this.state.data.toJS()} />); 
    } 
  }; 
} 

此模块定义了两个内部不可变对象:componentsstorecomponents列表包含对正在侦听store更改的组件的引用。store表示整个应用状态。

商店的概念源于Flux,这是一组用于构建大规模应用的架构模式。在本书中,我们将在这里和那里涉及到通量概念,但作为一个整体,通量远远超出了本书的范围。

该模块的重要部分是导出的函数:setState()getState()connect()getState()函数只返回对数据存储的引用。setState()函数设置存储的状态,然后通知所有组件应用的状态已更改。connect()函数是用新组件包装给定组件的高阶函数。安装组件时,它会向存储注册自身,以便在存储更改状态时接收更新。它通过将store作为属性传递来呈现合成组件。

现在,让我们使用此实用程序构建一个简单的过滤器和列表。首先,列表组件:

import React, { PropTypes } from 'react'; 

// Renders an item list... 
const MyList = ({ items }) => ( 
  <ul> 
    {items.map(i => ( 
      <li key={i}>{i}</li> 
    ))} 
  </ul> 
); 

MyList.propTypes = { 
  items: PropTypes.array.isRequired, 
}; 

export default MyList; 

这里没有太多你还没有看到的事情。现在让我们看看过滤器组件:

import React, { PropTypes } from 'react'; 
import { fromJS } from 'immutable'; 
import { getState, setState } from './store'; 

// When the filter input value changes. 
function onChange(e) { 
  // The state that we're working with... 
  const state = getState(); 
  const items = state.get('items'); 
  const tempItems = state.get('tempItems'); 

  // The new state that we're going to set on 
  // the store. 
  let newItems; 
  let newTempItems; 

  // If the input value is empty, we need to restore the 
  // items from "tempItems". 
  if (e.target.value.length === 0) { 
    newItems = tempItems; 
    newTempItems = fromJS([]); 
  } else { 
    // If "tempItems" hasn't been set, make sure that 
    // it gets the current items so that we can restore 
    // them later. 
    if (tempItems.isEmpty()) { 
      newTempItems = items; 
    } else { 
      newTempItems = tempItems; 
    } 

    // Filter and set "newItems". 
    const filter = new RegExp(e.target.value, 'i'); 
    newItems = items.filter(i => filter.test(i)); 
  } 

  // Updates the state of the store. 
  setState(state.merge({ 
    items: newItems, 
    tempItems: newTempItems, 
  })); 
} 

// Renders a simple input element to filter a list. 
const MyInput = ({ value, placeholder }) => ( 
  <input 
    autoFocus 
    value={value} 
    placeholder={placeholder} 
    onChange={onChange} 
  /> 
); 

MyInput.propTypes = { 
  value: PropTypes.string, 
  placeholder: PropTypes.string, 
}; 

export default MyInput; 

MyInput组件本身非常简单;它只是一个<input>元素。需要解释的是onChange处理程序,所以让我们在这里花点时间。此处理程序的目标是过滤用户列表,以便仅显示包含当前输入文本的项目。由于MyList组件实际上并不过滤传递给它的任何内容,因此该处理程序需要更改传递给它的内容。这是拥有一个保存应用状态的集中式存储的本质——不同组件如何通信。

我们处理过滤用户列表的方式包括在其他任何事情发生之前复制它。这是因为我们需要实际修改items状态。由于我们有原始的,这是很容易恢复以后。过滤完成后,我们使用setState()让其他组件知道应用的状态已经改变。

以下是呈现的过滤器输入和项目列表的外观:

Providing data sources

总结

在本章中,您了解了扩展现有组件的不同方法。您了解的第一个机制是继承。这是使用 ES2015 类语法完成的,对于实现通用方法或 JSX 标记非常有用。

然后,您了解了高阶组件,在高阶组件中,您使用一个函数将一个组件包装成另一个组件,从而为其提供新的功能。这是新 React 应用的发展方向,而不是继承和混合。

在下一章中,您将学习如何基于当前 URL 呈现组件。