State and Lifecycle

接下來讓我們重新看看之前的時鐘範例.

目前為止, 我們只有一招可以用來更新UI, 那就是ReactDOM.render():

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Try it on CodePen.

在本節當中, 我們會想辦法讓先前的時鐘範例變成一個自給自足的Clock component, 他可以做到自己每秒更新UI一次而不需要透過ReactDOM.render()!

第一步, 我們先把JSX的部分獨立出來:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Try it on CodePen.

當然, 現在這個Clock component仍舊依靠外來的props才得以更新UI, 這導致使用Clock的程式必須負責設置計時器(timer), 並取出秒數給Clock, 這並不是一個理想的方法.

我們真正希望的是Clock component可以被這樣使用:

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

要能達成我們的目標, 我們需要學習一個新觀念: component的state.

你可以將State想像成是專屬於component自己的資料, 由該component 100%控制.

譯註: 切記切記! 對component來說, props是從外部傳進來的資料, 而state是完全屬於component內部的資料!

要在component中使用state, 你必須用class component的方法來撰寫該component, 這也是class componentfunctional component的最大的差異.

Converting a Function to a Class

You can convert a functional component like Clock to a class in five steps:

  1. Create an ES6 class, with the same name, that extends React.Component.

  2. Add a single empty method to it called render().

  3. Move the body of the function into the render() method.

  4. Replace props with this.props in the render() body.

  5. Delete the remaining empty function declaration.

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Try it on CodePen.

Clock is now defined as a class rather than a function.

This lets us use additional features such as local state and lifecycle hooks.

Adding Local State to a Class

We will move the date from props to state in three steps:

  1. Replace this.props.date with this.state.date in the render() method:
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. Add a class constructor that assigns the initial this.state:
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Note how we pass props to the base constructor:

  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

Class components should always call the base constructor with props.

  1. Remove the date prop from the <Clock /> element:
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

We will later add the timer code back to the component itself.

The result looks like this:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Try it on CodePen.

Next, we’ll make the Clock set up its own timer and update itself every second.

Adding Lifecycle Methods to a Class

In applications with many components, it’s very important to free up resources taken by the components when they are destroyed.

We want to set up a timer whenever the Clock is rendered to the DOM for the first time. This is called “mounting” in React.

We also want to clear that timer whenever the DOM produced by the Clock is removed. This is called “unmounting” in React.

We can declare special methods on the component class to run some code when a component mounts and unmounts:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

These methods are called “lifecycle hooks”.

The componentDidMount() hook runs after the component output has been rendered to the DOM. This is a good place to set up a timer:

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

Note how we save the timer ID right on this.

While this.props is set up by React itself and this.state has a special meaning, you are free to add additional fields to the class manually if you need to store something that doesn’t participate in the data flow (like a timer ID).

We will tear down the timer in the componentWillUnmount() lifecycle hook:

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

Finally, we will implement a method called tick() that the Clock component will run every second.

It will use this.setState() to schedule updates to the component local state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Try it on CodePen.

Now the clock ticks every second.

Let’s quickly recap what’s going on and the order in which the methods are called:

  1. When <Clock /> is passed to ReactDOM.render(), React calls the constructor of the Clock component. Since Clock needs to display the current time, it initializes this.state with an object including the current time. We will later update this state.

  2. React then calls the Clock component’s render() method. This is how React learns what should be displayed on the screen. React then updates the DOM to match the Clock’s render output.

  3. When the Clock output is inserted in the DOM, React calls the componentDidMount() lifecycle hook. Inside it, the Clock component asks the browser to set up a timer to call the component’s tick() method once a second.

  4. Every second the browser calls the tick() method. Inside it, the Clock component schedules a UI update by calling setState() with an object containing the current time. Thanks to the setState() call, React knows the state has changed, and calls the render() method again to learn what should be on the screen. This time, this.state.date in the render() method will be different, and so the render output will include the updated time. React updates the DOM accordingly.

  5. If the Clock component is ever removed from the DOM, React calls the componentWillUnmount() lifecycle hook so the timer is stopped.

使用setState的正確方法

There are three things you should know about setState().

不要直接修改State

以下的程式碼並不會觸發UI的更動:

// Wrong
this.state.comment = 'Hello';

我們應該要使用setState():

// Correct
this.setState({comment: 'Hello'});

我們應該避免直接修改state, 唯一的例外是在constructor中第一次對state賦值時.

State的更新是非同步的

基於效能的理由, React會把多個setState()集合起來一次進行真正的UI更新.

因為this.propsthis.state可能會非同步的被更新, 基於他們的值來計算下次的state可能造成難解的bug

舉例來說, 下面這段程式碼可能造成不正確的state:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

當你遇到這種狀況時, 可以使用setState()的另一個形式: 傳入一個function (本來是傳入一個object). 這個function會在React更新下次state之前被呼叫, 並被傳入之前的state以及當下的props:

// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

在這個範例中我們使用了arrow function, 但其實使用一般的function也可以:

// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});

多個State的更新會被集合起來

When you call setState(), React merges the object you provide into the current state.

For example, your state may contain several independent variables:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

Then you can update them independently with separate setState() calls:

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

The merging is shallow, so this.setState({comments}) leaves this.state.posts intact, but completely replaces this.state.comments.

The Data Flows Down

Neither parent nor child components can know if a certain component is stateful or stateless, and they shouldn’t care whether it is defined as a function or a class.

This is why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it.

A component may choose to pass its state down as props to its child components:

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

This also works for user-defined components:

<FormattedDate date={this.state.date} />

The FormattedDate component would receive the date in its props and wouldn’t know whether it came from the Clock’s state, from the Clock’s props, or was typed by hand:

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

Try it on CodePen.

This is commonly called a “top-down” or “unidirectional” data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree.

If you imagine a component tree as a waterfall of props, each component’s state is like an additional water source that joins it at an arbitrary point but also flows down.

To show that all components are truly isolated, we can create an App component that renders three <Clock>s:

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Try it on CodePen.

Each Clock sets up its own timer and updates independently.

In React apps, whether a component is stateful or stateless is considered an implementation detail of the component that may change over time. You can use stateless components inside stateful components, and vice versa.