Fullstack React 学习笔记(五)

Advanced Component Configuration with props, state, and children

本章我们深度挖掘一下 components 的配置,ReactComponent 是一个 JavaScript 对象,它最少有一个 render() 函数,期待返回一个 ReactElement 对象。

回忆一下,ReactElement 是 DOM 元素在虚拟 DOM 中的一种表现形式

ReactComponent 主要目标是:

  • render() 一个 ReactElement(最终将成为真正的DOM)
  • 附加一些功能(functionality)包括:事件处理、管理 state、与子组件的交互

ReactComponent

Creating ReactComponents

我们可以使用两种方式创建 ReactComponent 实例:

第一种:createReactClass()

import React from 'react';
import createReactClass from 'create-react-class';

// React.createClass
const CreateClassApp = createReactClass({ 
    render: function() { } // required method
});

export default CreateClassApp;

第二种 ES6 classes

import React from 'react';

// ES6 class-style
class ComponentApp extends React.Component { 
    render() { } // required
}

export default ComponentApp;

render() Returns a ReactElement Tree

当 component 安装和初始化后,render() 将被调用。render() 函数的目标是提供 给 React 一个虚拟 DOM 组件

ES 5 风格

const CreateClassHeading = createReactClass({ 
    render: function() {
        return <h1>Hello</h1>;
    }
});

ES 6 风格

class Heading extends React.Component { 
    render() {
        return ( 
            <h1>Hello</h1>
        )
    }
};

如果返回的是一个 null 或 false,React 会将这种值渲染成一个空的元素 ,这通常用来从页面中移除标签

Getting Data into render()

在 React 中,props 是不可变的数据,它由父组件传递到子组件。state 是组件用来存放自己数据的地方,如果 state 发生变化,组件也会重新渲染。与 props 不同,state 是组件私有并且可变的。

接下来我们要谈一下 context,它是一种「隐式 props」,并在整个 component 树中传递。

props are the parameters

props 是组件的输入部分,如果把组件 component 想象成一个函数,那么 props 就是参数部分。

<div>
    <Header headerText="Hello world" /> 
</div>
是通常意义上的 DOM 元素,而
是 Header 组件的实例。在这个例子中,我们将数据(字符串 "Hello world")通过属性 headerText 传递给组件。

在子组件中,可以通过 this.props.headerText 获取到 headerText 属性

import React from 'react';

export class Header extends React.Component { 
    render() {
        return ( 
            <h1>{this.props.headerText}</h1>
        ); 
    }
}

可以通过 props 传递任意 JavaScript 对象,比如标量(primitives),简单的 JavaScript 对象,atoms,函数等。甚至还可以传递其他 React 元素和虚拟 DOM 节点。

PropTypes

我们可以通过使用 proptyes 指定每种 prop 的类型,在此工程的 package.json 中已经包括了 prop-types 的包

通过如下方式定义 PropTypes:

class MapComponent extends React.Component { 
    static propTypes = {
        lat: PropTypes.number,
        lng: PropTypes.number,
        zoom: PropTypes.number,
        place: PropTypes.object,
        markers: PropTypes.array
    };

指定的这些类型都是在 PropTypes 中的内置类型,我们也可以自定义类型,目前需要知道的类型有标量类型:

  • string
  • number
  • boolean

复杂一些的类型有:

  • funciton
  • object
  • array
  • arrayOf
  • node
  • element

我们还可以验证输入的内容是否为特定对象的实例

Default props with getDefaultProps()

有时需要 props 带一个默认值,此刻就可以使用 static defaultProps

class Counter extends React.Component { 
    static defaultProps = {
        initialValue: 1
    };
    // ...
};

这就等于:

<Counter />
<Counter initialValue={1} />

context

有时候需要一个全局的 prop,通常的做法是放置在父组件中然后向下传递到子组件。现在 context 将代替我们完成变量在组件中的传递工作。

context 特性还在实验阶段,它似乎会使用一个全局变量来处理应用的状态,因此还是尽量少用为妙

当我们指定一个 context,React 会自动管理 context 的传递工作,任意组件都能访问到这个全局 context

为了告诉 React 我们想将 context 从父组件传递到其余组件去,就需要在父组件中定义两个属性

  • childContextTypes
  • getChildContext 方法

而为了在子组件中获取 context,也需要在子组件中定义属性 contextTypes 。为了演示,举个聊天窗口的例子:

class Messages extends React.Component { 
  static propTypes = {
    users: PropTypes.array.isRequired,
    messages: PropTypes.array.isRequired
  };
  
  render() {
    return (
      <div>
         <ThreadList />
         <ChatWindow />
      </div>
    )
  } 
});

// ThreadList 和 ChatWindow 都是 React.Components 

如果没有 context,那么就要从 this.props.usersthis.props.messages 获取数据,现在改由从 context 获取。

在 Messages 组件中,定义了两个属性,现在需要告诉 React 我们的 context 类型是什么。

首先定义了 childContextTypes 关键字,它是一种键值(key-value)对象,列出了属性名作为 key,而对应的 PropType 作为值。

class Messages extends React.Component { 
  static propTypes = {
    users: PropTypes.array.isRequired,
    initialActiveChatIdx: PropTypes.number,
    messages: PropTypes.array.isRequired,
};

static childContextTypes = { 
  users: PropTypes.array, 
  userMap: PropTypes.object,
};

与 propTypes 类似,childContextTypes 只定义类型,并不指定具体的值,为了充实 this.context 具体的内容,我们需要定义第二个函数:getChildContext()

通过 getChildContext() 我们可以设置 context 的初始值(通过函数的返回值)

class Messages extends React.Component { 
  // ...
  static childContextTypes = { 
    users: PropTypes.array, 
    userMap: PropTypes.object,
  };
  // ...
  getChildContext() { 
    return {
      users: this.getUsers(),
      userMap: this.getUserMap(), 
    };
  }
  // ...
}

因为在父组件中 state 和 props 都是可变的,所有 context 也是可修改的,父组件上的 state 或 props 每次发生变化时都会调用 getChildContext() 方法;如果 context 更新,那么随后的子组件也会重新渲染。

为了让子组件知道获取 context,我们需要告诉 React 子组件需要访问它,即在子组件上定义 contextTypes ,如果子组件没有 contextTypes ,那么 React 就不知道将 context 发往该组件。

下面我们在两个子组件上定义 contextTypes

class ThreadList extends React.Component { 
    // ...
    static contextTypes = { 
        users: PropTypes.array,
    };
    // ...
}

class ChatWindow extends React.Component { 
    // ...
    static contextTypes = { 
        userMap: PropTypes.object
    };
    // ...
}

class ChatMessage extends React.Component { 
    // ...
    static contextTypes = { 
        userMap: PropTypes.object,
    };
    // ...
}

Bingo!现在我们可以从任意子组件中获取到 users 了,而不用再手动通过 props 向下传递了,在子组件中引用属性也不再是是 this.props.xxx,而是 this.context.xxx

class ThreadList extends React.Component { 
    // ...
    render() { 
        return (
            <div className={styles.threadList}>
            <ul className={styles.list}>
                {this.context.users.map((u, idx) => { 
                    return (
                        <UserListing 
                            onClick={this.props.onClick} 
                            key={idx}
                            index={idx}
                            user={u}
                        />
                    ); 
                })}
            </ul> 
        </div>
    ); 
}

如果组件中定义了 contextType 那么该组件的一系列生命周期方法 lifecycle methods 就多了个参数 nextContext

class ThreadList extends React.Component { 
    // ...
    static contextTypes = { 
        users: PropTypes.array,
    };
    // ...
    componentWillReceiveProps(nextProps, nextContext) {
        // ...
    }
    // ...
    shouldComponentUpdate(nextProps, nextState, nextContext) {
        // ...
    }
    // ...
    componentWillUpdate(nextProps, nextState, nextContext) {
        // ...
    }
    // ...
    componentDidUpdate(prevProps, prevState, prevContext) {
        // ...
    }

在一个 functional stateless component 中,context 会当做第二个参数传入,关于 stateless components(无状态组件)稍后会介绍

const ChatHeader = (props, context) => { 
    // ...
};

JavaScript 中使用全局变量并不是个好主意,不推荐滥用 context,最好是当一个全局变量需要保留的情况下使用,比如保持一个登陆用户

state

使用 state 构建自定义按钮

让我们看看如何将组件变得 stateful

const CREDITCARD = 'Creditcard'; 
const BTC = 'Bitcoin';

class Switch extends React.Component { 
  state = {
    payMethod: BTC,
  };
  
  render() { 
    return (
      <div className='switch'>
      <div className='choice'>Creditcard</div>
      <div className='choice'>Bitcoin</div> 
      Pay with: {this.state.payMethod}
      </div> 
    );
  } 
}

module.exports = Switch;

目前 Switch 组件变成了 stateful,并且保持了对 payment 的追踪

现在增加一点交互性,添加一个事件处理方法

return (
  <div className='switch'>
    <div
      className='choice' 
      onClick={this.select(CREDITCARD)} // add this
    >Creditcard</div>
    <div
        className='choice'
        onClick={this.select(BTC)} // ... and this 
    >Bitcoin</div>
    Pay with: {this.state.payMethod}
    </div> 
);

onClick 期待接收一个函数的执行结果(还是函数),来看下具体的 select 方法的定义

class Switch extends React.Component { 
  state = {
    payMethod: BTC,
  };
  
  select = (choice) => { 
    return (evt) => {
      // <-- handler starts here
      this.setState({ 
        payMethod: choice,
      }); 
    };
  };

select 函数做了两件事:

  1. 返回一个函数
  2. 使用了 setState

我们注意到属性 onClick 期待传入一个函数,但我们首先执行了这个函数,这是因为 select 函数执行后会返回一个函数。并且在这个返回的函数中设置 state(通过 setState)

注意,select 实际上在渲染时就调用了,但它返回传递给 onClick 的函数在点击按钮后才会执行

目前被选中的元素还不会有视觉指示,通常通过 CSS 应用一个 active。在添加 CSS 逻辑前,我们先使用函数来重构下组件,相对于把所有的渲染代码都放到 render() 函数中,将其抽象放入一个函数

  renderChoice = (choice) => {
    return (
      <div className='choice' onClick={this.select(choice)}>
        {choice}
      </div>
    );
  };

  render() {
    return (
      <div className='switch'>
        {this.renderChoice(CREDITCARD)}
        {this.renderChoice(BTC)}
        Pay with: {this.state.payMethod}
      </div>
    );
  }
}

接着将 .active 类添加到 <div> choice 组件中

  renderChoice = (choice) => {
    // create a set of cssClasses to apply
    const cssClasses = [];

    if (this.state.payMethod === choice) {
      cssClasses.push(styles.active); // add .active class
    }

    return (
      <div
        className='choice'
        onClick={this.select(choice)}
        className={cssClasses}
      >
        {choice}
      </div>
    );
  };

  render() {
    return (
      <div className='switch'>
        {this.renderChoice(CREDITCARD)}
        {this.renderChoice(BTC)}
        Pay with: {this.state.payMethod}
      </div>
    );
  }

我们使用 webpack 载入了 CSS(import styles from '../Switch.css');这意味着所有的样式文件都可以像对象一样访问,类似于 styles.active

Stateful components

在组件上定义 state 通常需要设置一个实例变量 this.state 在对象的原型类上,设置 state 的位置有两处,要么是类的属性,要么在构造函数中

按如下方法设置一个 stateful 组件

  1. 允许我们定义组件的初始 state
  2. 告诉 React 我们的组件将是 stateful 的,否则组件将会被看做是 stateless 的

在构造函数中

class InitialStateComponent extends React.Component { 
    // ...
    constructor(props) { 
        super(props)
        this.state = { 
            currentValue: 1, 
            currentUser: {
                name: 'Ari' 
            }
        } 
    }
    // ...
}

prop 的值不能修改只能在初始化时赋值:

const CounterWrapper = props => ( 
    <div key="counterWrapper">
        <Counter initialValue={125} />
  </div>
);

在子组件 Counter 中,我们知道这只是个初始值,可能会发生变化,再将其设置为 state

class Counter extends Component { 
    constructor(props) {
        super(props);
        
        this.state = {
            value: this.props.initialValue
        };
        
        this.increment = this.increment.bind(this);
        this.decrement = this.decrement.bind(this); 
    }
        // ...
}

由于构造函数只运行一次,并且在组件安装前运行,因此可以使用它来构建初始值

State updates that depend on the current state

Counter 组件是一个计数器

按下 "-" 按钮的时候,React 会执行 decrement() 函数

decrement = () => {
    // Appears correct, but there is a better way 
    const nextValue = this.state.value - 1; 
    this.setState({
        value: nextValue
    });
};

如果 state 的更新依赖于现有的 state 时,最好的方式是传入一个函数来 setState()

class Counter extends Component {
  constructor(props) {
    super(props);

    this.state = {
      value: this.props.initialValue
    };

    this.increment = this.increment.bind(this);
    this.decrement = this.decrement.bind(this);
  }

  decrement = () => {
    this.setState(prevState => {
      return {
        value: prevState.value - 1
      };
    });
  };

setState(updater, callback) 带两个参数,这两个参数都是函数;第一个 updater 函数是 (prevState, props) => stateChange,这里也只用到了 updater 函数

为什么要大费周折这么做呢,因为 setState() 是异步执行的,state 并不会立即更新,而是 React 会将要更新的 state 入队列,如果存在一些高优先级的任务,就会导致 state 的更新时间变长

一旦 state 的变化取决于当前其他 state 时,最好使用函数设置 state 来避免错误产生

将 setState() 看做是一种请求,而不是立即更新组件的命令

Thinking About State

组件 state 必须是本地化的,它不能从外部组件获取,也不会传递给其他组件

React 组件中的 propsstate 可以考虑为:props 是组件的「参数」,而 state 是对象的「实例变量」;还有一点要牢记 state 里的东东越小,越容易序列化成 JSON,app 的速度就越快,也容易理解。

无状态组件(Stateless Components)

构建有状态组件的另一种方法是使用无状态组件(Stateless Components),无状态组件是 React 轻量级构建组件的方式,它仅需要一个 render() 方法

const Header = function(props) { 
    return (<h1>{props.headerText}</h1>)
}

这里访问 props 时并没有通过 this 来引用,仅仅是作为参数传递给函数;无状态组件实际上并不是类,某种意义上也不是 ReactElement

当 stateless components 以函数形态工作时,并没有引用 this;它只是个函数也没有使用实例,这种组件不包含 state,也不会随组件的生命周期方法被调用。在无状态组件中是可以使用 propTypesdefaultProps

为什么需要无状态组件呢?首先使用无状态组件能尽可能地控制 state 出现的数量;另外使用函数式组件有助于提高性能。

如果你不需要生命周期方法,并且只需要一个渲染函数 render,那么选择 stateless component 是不错的选择

  • 无状态组件不会被实例化,整体渲染性能得到提升
  • 无状态组件不能访问 this 对象
  • 无状态组件无法访问生命周期的方法
  • 无状态组件只能访问输入的props,同样的props会得到同样的渲染结果,不会有副作用

React创建组件的三种方式及其区别

切换到 Stateless

可以将 Switch 组件转换成 stateless component 吗?目前选中的 payment choice 是 state,需要放置在其他地方。

我们不能完全移除 state,但至少能孤立它。React 的通用做法是将 state 移到少数的父组件上;在之前的 Switch 组件中,将每一个 choice 都放到了 renderChoice 函数中,这说明了 choice 可以放到我们的 stateless component 中。不过还存在一个问题:renderChoice 方法调用了 select,而 select 又调用了 setState(等于 renderChoice 间接修改了 state),让我们看看如何解决这个问题:

const Choice = function (props) { 
    const cssClasses = [ ];
    
    if (props.active) {
        // <-- check props, not state 
        cssClasses.push(styles.active);
    }
    
    return ( 
        <div
            className='choice'
            onClick={props.onClick}
            className={cssClasses}
        >
        {props.label} {/* <-- allow any label */}

这里创建了一个 Choice 函数,它是一个 stateless component,但是存在一个问题:怎么读取 state 的值呢?这里的解决方式是通过 props 传递一个参数进去

在 Choice 中,我们做了三方面的改变:

  1. 通过读取props.active 来决定 choice 是否是被选中(active 状态)
  2. 当 Choice 被点击,调用的函数变成了 props.onClick
  3. 显示额你让变成了 props.label

以上所有努力都有一个共同目标:让 Choice 从 Switch 组件中去耦合,现在可以令人信服的在任意地方使用 Choice 组件了(只需通过 props 传入 active,onClick,以及 label),下面看一下父组件 Switch

render() { 
    return (
        <div className='switch'>
            <Choice
                onClick={this.select(CREDITCARD)} 
                active={this.state.payMethod === CREDITCARD} 
                label='Pay with Creditcard'
            />
            
            <Choice
                onClick={this.select(BTC)} 
                active={this.state.payMethod === BTC} 
                label='Pay with Bitcoin'
            />
            
            Paying with: {this.state.payMethod}

通过创建一个无状态组件 stateless component Choice,我们能够使 Choice 变得可重用,并且不绑定任意 state

无状态鼓励重用

Stateless components 无状态组件是一种创建可重用组件的方式。因为无状态组件所需的一切都是由外部引入的

Talking to Children Components with props.children

this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点。在组件中,我们可以通过 this.props.children 引用子组件,例如存在一个 Newspaper 组件包含了一个 Article:

const Newspaper = props => { 
    return (
        <Container>
            <Article headline="An interesting Article">
                Content Here
            </Article>
        </Container>
    ) 
}

为了在 Article 周围添加点标记,我们创建了 Container 组件来包含了其下的所有子组件

class Container extends React.Component { 
    render() {
        return <div className="container">{this.props.children}</div>;
}

这里需要注意, this.props.children 的值有三种可能:如果当前组件没有子节点,它就是 undefined ;如果有一个子节点,数据类型是 object ;如果有多个子节点,数据类型就是 array 。所以,处理 this.props.children 的时候要小心。

React 提供一个工具方法 React.Children 来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。

现在来改写下之前的 Container 组件,使用 propTypes 来注释 API,这里可以包含多个 Article 组件,也可以是单个 Article 组件,来设置 children prop 要么是一个元素,要么是一个数组。

class DocumentedContainer extends React.Component { 
    static propTypes = {
        children: PropTypes.oneOf([PropTypes.element, PropTypes.array])
    };
    //...
    render() {
        return <div className="container">{this.props.children}</div>;
    }
}

也可以直接了当要求 children 是单一元素

class SingleChildContainer extends React.Component { 
    static propTypes = {
        children: PropTypes.element.isRequired
    };
    //...
    render() {
        return <div className="container">{this.props.children}</div>;
    } 
}

React.Childern 也提供了很多便利方法(helpr methods),我们来看几个

React.Children.map() & React.Children.forEach()

让我们继续重写 Container 组件,以便为每个孩子提供一个可配置的包装器组件

这里调用了 React.createElement( ) 为每个子组件生成了一个新的 ReactElement

class MultiChildContainer extends React.Component { 
    static propTypes = {
        component: PropTypes.element.isRequired,
        children: PropTypes.element.isRequired
    };
    //...
    renderChild = (childData, index) => { 
        return React.createElement(
            this.props.component,
            {}, // <~ child props
            childData // <~ child's children
        ); 
    };
    // ...
    render() { 
        return (
            <div className="container"> {
                React.Children.map(this.props.children, this.renderChild)}
            </div> 
        );
    } 
}
  • component 代表了要封装的每一个单独的组件
  • children 表示要封装的全部孩子组件列表

React.Children.toArray()

props.children 可能返回一个非常棘手的数据结构,通常我们会将其转换成一个标准的数组,例如想要重新排序子元素时,可以使用 React.Children.toArray( )props.children 数据结构转换成子组件的数组

class ArrayContainer extends React.Component { 
    static propTypes = {
        component: PropTypes.element.isRequired,
        children: PropTypes.element.isRequired
    };
    //...
    render() {
        const arr = React.Children.toArray(this.props.children);
        return <div className="container">{arr.sort((a, b) => a.id < b.id)}</div>; 
    }
}

总结

通过使用 props 和 context 我们可以从组件中获取数据,而使用 PropTypes 我们可以很明晰地指出数据的类型;将 state 变成组件自有数据,然后当 state 发生变化时重新渲染;一种减少 stateful 组件的办法是抽象出更多的无状态组件。


-EOF-

walkingway

Read more posts by this author.

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!