三、理解属性和状态

React 组件依赖于 JSX 语法,该语法用于描述 UI 的结构。JSX 只会让我们了解到目前为止,我们需要数据来填充 React 组件的结构。本章的重点是组件数据,它有两种类型:属性状态

我们将从定义属性和状态的含义开始。然后,我们将通过一些示例演示设置组件状态和传递组件属性的机制。在本章的最后,我们将以您新发现的道具和状态知识为基础,介绍功能组件和容器模式。

什么是组件状态?

React 组件使用 JSX 声明 UI 元素的结构。但这只是故事的一部分。组件需要数据才能发挥作用。例如,组件 JSX 可能会声明一个将 JavaScript 集合映射到<li>元素的<ul>。这个收藏来自哪里?

状态是 React 组件的动态部分。这意味着您可以声明组件的初始状态,该状态会随时间而变化。

假设我们正在呈现一个组件,其中一段状态被初始化为空数组。稍后,此数组将填充数据。这被称为状态的改变,每当我们告诉 React 组件改变其状态时,该组件将自动重新渲染自身。此处显示了该过程:

What is component state?

组件的状态是组件本身可以设置的,或者是组件外部的其他代码。现在我们来看看组件属性以及它们与组件状态的区别。

什么是组件属性?

属性用于将数据传递到您的组件中。属性只在呈现组件时传递,而不是调用以新状态作为参数值的方法。也就是说,我们将属性值传递给 JSX 元素。

在 JSX 的上下文中,属性被称为属性,可能是因为用 XML 的说法就是这样。在本书中,属性和属性是同义的。

属性与状态不同,因为它们不是在组件初始渲染后更改的内容。如果属性值已更改,并且我们希望重新渲染组件,那么我们必须重新渲染用于渲染组件的 JSX。React 堆内构件负责确保有效完成此操作。以下是使用属性渲染和重新渲染组件的图示:

What are component properties?

这看起来与有状态组件有很大不同。真正的区别在于,对于属性,通常由父组件决定何时呈现 JSX。组件实际上不知道如何重新渲染自身。正如我们将在本书中看到的那样,这种自上而下的流动比到处变化的状态更容易预测。

有了介绍性的解释,让我们通过编写一些代码来理解这两个概念。

设置组件状态

在本节中,您将编写一些 React 代码来设置组件的状态。首先,您将了解初始状态这是组件的默认状态。接下来,您将学习如何更改组件的状态,使其重新渲染自身。最后,您将看到新状态如何与现有状态合并。

初始组件状态

组件的初始状态实际上不是必需的,但如果组件使用状态,则应该设置它。这是因为,如果组件 JSX 期望某些状态属性存在,而它们不存在,那么组件将失败或呈现意外的结果。谢天谢地,设置初始组件状态很容易。

组件的初始状态应始终是具有一个或多个属性的对象。例如,您可能有一个使用单个数组作为其状态的组件。这很好,但只需确保将初始数组设置为 state 对象的属性即可。不要使用数组作为状态。原因很简单:一致性。每个 react 组件都使用一个普通对象作为其状态。

现在让我们把注意力转向一些代码。下面是一个设置初始状态对象的组件:

import React, { Component } from 'react'; 

export default class MyComponent extends Component { 
  // The initial state is set as a simple property 
  // of the component instance. 
  state = { 
    first: false, 
    second: true, 
  } 

  render() { 
    // Gets the "first" and "second" state properties 
    // into constants, making our JSX less verbose. 
    const { first, second } = this.state; 

    // The returned JSX uses the "first" and "second" 
    // state properties as the "disabled" property 
    // value for their respective buttons. 
    return ( 
      <main> 
        <section> 
          <button disabled={first}>First</button> 
        </section> 
        <section> 
          <button disabled={second}>Second</button> 
        </section> 
      </main> 
    ); 
  } 
} 

如果您查看由render()返回的 JSX,您实际上可以看到该组件所依赖的状态值-firstsecond。因为我们在初始状态下设置了这些属性,所以渲染组件是安全的,不会有任何意外。例如,我们只能渲染此组件一次,由于初始状态,它将按预期渲染:

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

import MyComponent from './MyComponent'; 

// "MyComponent" has an initial state, nothing is passed 
// as a property when it's rendered. 
render( 
  (<MyComponent />), 
  document.getElementById('app') 
); 

以下是渲染输出的外观:

Initial component state

设置初始状态不是很令人兴奋,但它仍然很重要。让我们使组件在状态更改时重新渲染自身。

设置组件状态

让我们创建一个具有一些初始状态的组件。然后,我们将渲染该组件,并更新其状态。这意味着组件将渲染两次。让我们看一下组件,这样你就可以看到我们在这里工作的内容:

import React, { Component } from 'react'; 

export default class MyComponent extends Component { 
  // The initial state is used, until something 
  // calls "setState()", at which point the state is 
  // merged with this state. 
  state = { 
    heading: 'React Awesomesauce (Busy)', 
    content: 'Loading...', 
  } 

  render() { 
    const { heading, content } = this.state; 

    return ( 
      <main> 
        <h1>{heading}</h1> 
        <p>{content}</p> 
      </main> 
    ); 
  } 
} 

如您所见,该组件的 JSX 依赖于两个状态属性-headingcontent。该组件还设置这两个状态属性的初始值,这意味着可以在没有任何意外陷阱的情况下渲染它。现在,让我们看一些呈现组件的代码,然后通过更改状态重新呈现组件:

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

import MyComponent from './MyComponent'; 

// The "render()" function returns a reference to the 
// rendered component. In this case, it's an instance 
// of "MyComponent". Now that we have the reference, 
// we can call "setState()" on it whenever we want. 
const myComponent = render( 
  (<MyComponent />), 
  document.getElementById('app') 
); 

// After 3 seconds, set the state of "myComponent", 
// which causes it to re-render itself. 
setTimeout(() => { 
  myComponent.setState({ 
    heading: 'React Awesomesauce', 
    content: 'Done!', 
  }); 
}, 3000); 

组件首先以其默认状态呈现。然而,这段代码中有趣的地方是setTimeout()调用。3 秒后,使用setState()更改两个状态属性值。果不其然,这种变化会反映在 UI 中。以下是渲染时的初始状态:

Setting component state

以下是状态更改后渲染输出的外观:

Setting component state

本例强调了使用声明性 JSX 语法来描述 UI 组件结构的强大功能。我们声明它一次,并随着时间的推移更新组件的状态,以反映应用中发生的更改。所有的 DOM 交互都经过优化并隐藏在视图中。很酷吧?

在本例中,我们实际上替换了整个组件状态。也就是说,对setState()的调用以初始状态中找到的相同对象属性传递。但是,如果我们只想更新组件状态的一部分呢?

合并组件状态

设置 React 组件的状态时,实际上是将组件的状态与传递给setState()的对象合并。这很有用,因为这意味着您可以设置组件状态的一部分,而保持其余状态不变。现在让我们看一个例子。首先,具有某种状态的组件:

import React, { Component } from 'react'; 

export default class MyComponent extends Component { 
  // The initial state... 
  state = { 
    first: 'loading...', 
    second: 'loading...', 
    third: 'loading...', 
  } 

  render() { 
    const { state } = this; 

    // Renders a list of items from the 
    // component state. 
    return ( 
      <ul> 
        {Object.keys(state).map(i => ( 
          <li key={i}> 
            <strong>{i}: </strong>{state[i]} 
          </li> 
        ))} 
      </ul> 
    ); 
  } 
} 

该组件呈现其状态的键和值。每个值默认为loading...,因为我们还不知道该值。让我们编写一些代码,分别设置每个状态属性的状态:

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

import MyComponent from './MyComponent'; 

// Stores a reference to the rendered component... 
const myComponent = render( 
  (<MyComponent />), 
  document.getElementById('app') 
); 

// Change part of the state after 1 second... 
setTimeout(() => { 
  myComponent.setState({ first: 'done!' }); 
}, 1000); 

// Change another part of the state after 2 seconds... 
setTimeout(() => { 
  myComponent.setState({ second: 'done!' }); 
}, 2000); 

// Change another part of the state after 3 seconds... 
setTimeout(() => { 
  myComponent.setState({ third: 'done!' }); 
}, 3000); 

本例的优点是,您可以在组件上设置各个状态属性。它将有效地重新呈现自己。以下是初始组件状态的渲染输出:

Merging component state

以下是两次setTimeout()回调运行后的输出:

Merging component state

传递属性值

属性就像状态一样,它们是传递到组件中的数据。但是,属性与状态的不同之处在于,在渲染组件时,它们只设置一次。在本节中,我们将了解默认属性值。然后,我们来看看设置属性值。在本节之后,您应该能够掌握组件状态和属性之间的差异。

默认属性值

默认特性值的工作方式与默认状态值略有不同。它们被设置为一个名为defaultProps的类属性。让我们来看一个声明缺省属性值的组件:

import React, { Component } from 'react'; 

export default class MyButton extends Component { 
  // The "defaultProps" values are used when the 
  // same property isn't passed to the JSX element. 
  static defaultProps = { 
    disabled: false, 
    text: 'My Button', 
  } 

  render() { 
    // Get the property values we want to render. 
    // In this case, it's the "defaultProps", since 
    // nothing is passed in the JSX. 
    const { disabled, text } = this.props; 

    return ( 
      <button disabled={disabled}>{text}</button> 
    ); 
  } 
} 

那么,为什么不像默认状态那样,将默认属性值设置为实例属性呢?原因是属性是不可变的,不需要作为实例属性值保存。另一方面,状态一直在更改,因此组件需要对其进行实例级引用。

您可以看到,该组件为disabledtext设置了默认属性值。这些值仅在未通过用于呈现组件的 JSX 标记传入时使用。让我们继续渲染这个没有任何 JSX 属性的组件,以确保使用了defaultProps值。

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

import MyButton from './MyButton'; 

// Renders the "MyButton" component, without 
// passing any property values. 
render( 
  (<MyButton />), 
  document.getElementById('app') 
); 

始终具有默认状态的原则同样适用于属性。您希望能够渲染组件,而不必事先知道组件的动态值。现在,我们将把注意力转向在 React 组件上设置属性值。

设置属性值

首先,让我们创建一对期望不同类型属性值的组件。

第 7 章验证组件属性中,我们将更详细地讨论验证传递给组件的属性值。

import React, { Component } from 'react'; 

export default class MyButton extends Component { 

  // Renders a "<button>" element using values 
  // from "this.props". 
  render() { 
    const { disabled, text } = this.props; 

    return ( 
      <button disabled={disabled}>{text}</button> 
    ); 
  } 
} 

这个简单的按钮组件需要一个布尔disabled属性和一个字符串text属性。让我们再创建一个需要数组属性值的组件:

import React, { Component } from 'react'; 

export default class MyList extends Component { 
  render() { 

    // The "items" property is an array. 
    const { items } = this.props; 

    // Maps each item in the array to a list item. 
    return ( 
      <ul> 
        {items.map(i => ( 
          <li key={i}>{i}</li> 
        ))} 
      </ul> 
    ); 
  } 
} 

如您所见,我们可以通过 JSX 传递任何我们想要的属性值,只要它是一个有效的 JavaScript 表达式。现在,让我们编写一些代码来设置这些属性值:

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

// The two components we're to passing props to 
// when they're rendered. 
import MyButton from './MyButton'; 
import MyList from './MyList'; 

// This is the "application state". This data changes 
// over time, and we can pass the application data to 
// components as properties. 
const appState = { 
  text: 'My Button', 
  disabled: true, 
  items: [ 
    'First', 
    'Second', 
    'Third', 
  ], 
}; 

// Defines our own "render()" function. The "renderJSX()" 
// function is from "react-dom" and does the actual 
// rendering. The reason we're creating our own "render()" 
// function is that it contains the JSX that we want to 
// render, and so we can call it whenever there's new 
// application data. 
function render(props) { 
  renderJSX(( 
    <main> 
      { /* The "MyButton" component relies on the "text" 
           and the "disabed" property. The "text" property 
           is a string while the "disabled" property is a 
           boolean. */ } 
      <MyButton 
        text={props.text} 
        disabled={props.disabled} 
      /> 

      { /* The "MyList" component relies on the "items" 
           property, which is an array. Any valid 
           JavaScript data can be passed as a property. */ } 
      <MyList items={props.items} /> 
    </main> 
    ), 
    document.getElementById('app') 
  ); 
} 

// Performs the initial rendering... 
render(appState); 

// After 1 second, changes some application data, then 
// calls "render()" to re-render the entire structure. 
setTimeout(() => { 
  appState.disabled = false; 
  appState.items.push('Fourth'); 
  render(appState); 
}, 1000); 

render()函数看起来每次调用时都在创建新的 React 组件实例。React 足够聪明,可以看出这些组件已经存在,只需要知道输出与新属性值之间的差异。React 非常强大,我提到过吗?

这个例子的另一个优点是我们有一个appState对象,它保持应用的状态。渲染组件时,此状态的片段将作为属性传递到组件中。状态必须存在于某个地方,在本例中,我们将其移到组件之外。在下一节实现无状态功能组件时,我们将以这个主题为基础。

无状态组件

到目前为止,您在本书中看到的组件都是扩展基本Component类的类。是时候了解 React 中的功能组件了。在本节中,您将通过实现一个纯功能组件来了解什么是纯功能组件。然后,我们将介绍如何为无状态功能组件设置默认属性值。

纯功能部件

一个功能性组件就是它听起来的样子——一个功能。想象一下你所看到的任何 React 组件的render()方法。这种方法本质上就是组件。功能性 React 组件的工作是返回 JSX,就像基于类的 React 组件一样。不同之处在于,这是功能组件所能做的一切。它没有状态,也没有生命周期方法。

我们为什么要使用功能组件?这是最简单的问题。如果您的组件依赖于某些属性来呈现某些 JSX,而不做其他事情,那么当函数更简单时,为什么还要麻烦使用类呢?

纯功能是一种无副作用的功能。也就是说,使用给定的一组参数调用函数时,该函数总是生成相同的输出。这与 React 组件相关,因为给定一组属性,更容易预测渲染内容。

现在让我们来看一个功能组件:

import React from 'react'; 

// Exports an arrow function that returns a 
// "<button>" element. This function is pure 
// because it has no state, and will always 
// produce the same output, given the same 
// input. 
export default ({ disabled, text }) => ( 
  <button disabled={disabled}>{text}</button> 
); 

简洁,不是吗?此函数返回一个<button>元素,使用传入的属性作为参数(而不是通过this.props访问它们)。此函数是纯函数,因为如果传递相同的disabledtext属性值,则呈现相同的内容。现在,让我们看看如何渲染此组件:

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

// "MyButton" is a function, instead of a 
// "Component" subclass. 
import MyButton from './MyButton'; 

// Renders two "MyButton" components. We only need 
// the "first" and "second" properties from the 
// props argument by destructuring it. 
function render({ first, second }) { 
  renderJSX(( 
    <main> 
      <MyButton 
        text={first.text} 
        disabled={first.disabled} 
      /> 
      <MyButton 
        text={second.text} 
        disabled={second.disabled} 
      /> 
    </main> 
    ), 
    document.getElementById('app') 
  ); 
} 

// Reders the components, passing in property data. 
render({ 
  first: { 
    text: 'First Button', 
    disabled: false, 
  }, 
  second: { 
    text: 'Second Button', 
    disabled: true, 
  }, 
});

正如您所看到的,从 JSX 的角度来看,基于类和基于函数的 React 组件之间没有区别。无论组件是使用类语法还是函数语法声明的,JSX 看起来都完全相同。

约定是使用箭头函数语法来声明函数组件。然而,如果传统 JavaScript 函数语法更适合您的风格,那么使用传统 JavaScript 函数语法声明它们是完全有效的。

以下是呈现的 HTML 的外观:

Pure functional components

功能组件中的默认值

功能部件重量轻;它们没有任何状态或生命周期。但是,它们确实支持一些元数据选项。例如,我们可以使用与基于类的组件相同的方法指定功能组件的默认属性值。下面是一个这样的示例:

import React from 'react'; 

// The functional component doesn't care if the property 
// values are the defaults, or if they're passed in from 
// JSX. The result is the same. 
const MyButton = ({ disabled, text }) => ( 
  <button disabled={disabled}>{text}</button> 
); 

// The "MyButton" constant was created so that we could 
// attach the "defaultProps" metadata here, before 
// exporting it. 
MyButton.defaultProps = { 
  text: 'My Button', 
  disabled: false, 
}; 

export default MyButton; 

defaultProps属性位于函数而不是类上。当 React 遇到具有此属性的功能组件时,它知道如果没有通过 JSX 提供默认值,则会传入默认值。

集装箱组件

在本章的最后一节中,我们将介绍容器组件的概念。这是一种常见的 React 模式,它将您所了解的有关状态和属性的许多概念结合在一起。

容器组件的基本前提很简单:不要将数据获取与呈现数据的组件耦合。容器负责获取数据并将其传递给其子组件。它包含负责呈现数据的组件。

我们的想法是,您应该能够通过这种模式实现某种程度的可替代性。例如,容器可以替换其子组件。或者,子组件可以在不同的容器中使用。让我们从容器本身开始,看看容器模式的作用:

import React, { Component } from 'react'; 

import MyList from './MyList'; 

// Utility function that's intended to mock 
// a service that this component uses to 
// fetch it's data. It returns a promise, just 
// like a real async API call would. In this case, 
// the data is resolved after a 2 second delay. 
function fetchData() { 
  return new Promise((resolve) => { 
    setTimeout(() => { 
      resolve([ 
        'First', 
        'Second', 
        'Third', 
      ]); 
    }, 2000); 
  }); 
} 

// Container components usually have state, so they 
// can't be declared as functions. 
export default class MyContainer extends Component { 

  // The container should always have an initial state, 
  // since this will be passed down to child components 
  // as properties. 
  state = { items: [] } 

  // After the component has been rendered, make the 
  // call to fetch the component data, and change the 
  // state when the data arrives. 
  componentDidMount() { 
    fetchData() 
      .then(items => this.setState({ items })); 
  } 

  // Renders the containee, passing the container 
  // state as properties, using the spread operator: "...". 
  render() { 
    return ( 
      <MyList {...this.state} /> 
    ); 
  } 
} 

该组件的任务是获取数据并设置其状态。无论何时设置状态,都会调用render()。这就是子组件的用武之地。容器的状态作为属性传递给子容器。让我们来看看下一个组件:

import React from 'react'; 

// A stateless component that expects 
// an "items" property so that it can render 
// a "<ul>" element. 
export default ({ items }) => ( 
  <ul> 
    {items.map(i => ( 
      <li key={i}>{i}</li> 
    ))} 
  </ul> 
); 

没什么大不了的;一个简单的功能组件,需要一个items属性。让我们看看容器组件是如何实际使用的:

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

import MyContainer from './MyContainer'; 

// All we have to do is render the "MyContainer" 
// component, since it looks after providing props 
// for it's children. 
render( 
  (<MyContainer />), 
  document.getElementById('app') 
); 

我们将在第 5 章制作可重用组件中深入探讨容器组件设计。本示例的目的是让您了解 React 组件中状态和属性之间的相互作用。

加载页面时,您将在模拟 HTTP 请求所需的 3 秒钟后看到以下内容:

Container components

总结

在本章中,您了解了 React 组件中的状态和属性。我们从定义和比较这两个概念开始。然后,您实现了几个 React 组件并操纵它们的状态。接下来,通过实现将属性值从 JSX 传递到组件的代码,您了解了属性。最后,向您介绍了容器组件的概念,该组件用于将数据获取与呈现内容解耦。

在下一章中,您将了解如何在 React 组件中处理用户事件。