State 和生命週期
嘗試新的 React 文件。
這些新的文件頁面教導 modern React 並包括即時範例:
新的文件將會很快取代目前的文件,它將會被歸檔。提供回饋。
這個章節會介紹在 React component 中 state 以及生命週期的概念。你可以在這裡找到 component API 詳細的參考。
思考前一章節的 ticking clock 的範例。在 Rendering Elements 中,我們只學習到一種方式來更新 UI。 我們呼叫 root.render() 來改變 render 的輸出:
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);}
setInterval(tick, 1000);在這個章節中,我們將會學習如何封裝 Clock component 讓它可以真正的被重複使用。它將會設定本身的 timer 並且每秒更新一次。
我們可以像這樣封裝 clock 做為開始:
const root = ReactDOM.createRoot(document.getElementById('root'));
function Clock(props) {
  return (
    <div>      <h1>Hello, world!</h1>      <h2>It is {props.date.toLocaleTimeString()}.</h2>    </div>  );
}
function tick() {
  root.render(<Clock date={new Date()} />);}
setInterval(tick, 1000);然而,它缺少了一個重要的需求:Clock 設定 timer 並在每秒更新 UI 應該是 Clock 實作的細節的事實。
理想情況下,我們想要撰寫一次 Clock 並且它會自己更新:
root.render(<Clock />);如果要實現這個理想情況,我們需要加入「state」到 Clock component。
State 類似於 prop,但它是私有且由 component 完全控制的。
我們在先前提到過,component 被定義為 class 有一些額外的特性。Local state 就是 class 其中的一個特性。
轉換 Function 成 Class
你可以透過以下 5 個步驟轉換一個 function component 像是 Clock 成為 class:
- 建立一個相同名稱並且繼承 React.Component的 ES6 class。
- 加入一個 render()的空方法。
- 將 function 的內容搬到 render()方法。
- 將 render()內的props替換成this.props。
- 刪除剩下空的 function 宣告。
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}Clock 現在被定義成 class 而不是 function。
在每次發生更新時,render 方法都會被呼叫,但我們只要 render <Clock /> 到相同的 DOM node 中,只有 Clock class 這個實例會被用到。這讓我們可以使用像是 local state 和生命週期方法這些額外的特性。
加入 Local State 到 Class
我們會透過以下 3 個步驟將 date 從搬移到 state:
- 將 render()方法內的this.props.date替換成this.state.date:
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      </div>
    );
  }
}- 加入一個 class constructor 並分配初始的 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>
    );
  }
}注意,我們將傳送 props 到基礎 constructor:
  constructor(props) {
    super(props);    this.state = {date: new Date()};
  }Class component 應該總是要呼叫基礎 constructor 和 props。
- 從 <Clock />element 中移除dateprop:
root.render(<Clock />);之後我們將會把 timer 的程式碼加入到 component 本身。
結果看起來會像是:
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>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);接下來,我們會讓 Clock 設定它本身的 timer 並且每秒更新一次。
加入生命週期方法到 Class
在具有許多 component 的應用程式中,當 component 被 destroy 時,釋放所佔用的資源是非常重要的。
每當 Clock render 到 DOM 的時候,我們想要設定一個 timer。在 React 中稱為「mount」。
每當產生的 Clock DOM 被移除時,我們想要清除 timer。在 React 中稱為「unmount」。
每當 component 在 mount 或是 unmount 的時候,我們可以在 component class 上宣告一些特別的方法來執行一些程式碼:
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>
    );
  }
}這些方法被稱為「生命週期方法」。
componentDidMount() 方法會在 component 被 render 到 DOM 之後才會執行。這是設定 timer 的好地方:
  componentDidMount() {
    this.timerID = setInterval(      () => this.tick(),      1000    );  }注意我們是如何正確的在 this(this.timerID) 儲存 timer ID。
雖然 this.props 是由 React 本身設定的,而且 this.state 具有特殊的意義,如果你需要儲存一些不相關於資料流的內容(像是 timer ID),你可以自由的手動加入。
我們將會在 componentWillUnmount() 生命週期方法內移除 timer:
  componentWillUnmount() {
    clearInterval(this.timerID);  }最後,我們將會實作一個 tick() 的方法,Clock component 將會在每秒執行它。
它將會使用 this.setState() 來安排 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>
    );
  }
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);現在我們的 clock 每秒鐘都會滴答作響。
讓我們快速的回顧一下發生了哪些事情,以及呼叫這些方法的順序:
- 當 <Clock />被傳入到root.render()時,React 會呼叫Clockcomponent 的constructor。由於Clock需要顯示目前的時間,它使用包含目前時間的 object 初始化this.state。我們會在之後更新這個 state。
- React 接著呼叫 Clockcomponent 的render()方法。這就是 React 如何了解應該要在螢幕上顯示什麼內容。React 接著更新 DOM 來符合Clock的 render 輸出。
- 每當 Clock輸出被插入到 DOM 時,React 會呼叫componentDidMount()生命週期方法。在Clockcomponent 生命週期方法內,會要求瀏覽器設定 timer 每秒去呼叫 component 的tick()方法。
- 瀏覽器每秒呼叫 tick()方法。其中,Clockcomponent 透過包含目前時間的 object 呼叫setState()來調度 UI 更新。感謝setState(),React 現在知道 state 有所改變,並且再一次呼叫render()方法來了解哪些內容該呈現在螢幕上。這時候,在render()方法內的this.state.date將會有所不同,因此 render 輸出將會是更新的時間。React 相應地更新 DOM。
- 如果 Clockcomponent 從 DOM 被移除了,React 會呼叫componentWillUnmount()生命週期方法,所以 timer 會被停止。
正確的使用 State
有三件關於 setState() 的事情你應該要知道。
請不要直接修改 State
例如,這將不會重新 render component:
// 錯誤
this.state.comment = 'Hello';相反的,使用 setState():
// 正確
this.setState({comment: 'Hello'});你唯一可以指定 this.state 值的地方是在 constructor。
State 的更新可能是非同步的
React 可以將多個 setState() 呼叫批次處理為單一的更新,以提高效能。
因為 this.props 和 this.state 可能是非同步的被更新,你不應該依賴它們的值來計算新的 state。
例如,這個程式碼可能無法更新 counter:
// 錯誤
this.setState({
  counter: this.state.counter + this.props.increment,
});要修正這個問題,使用第二種形式的 setState(),它接受一個 function 而不是一個 object。Function 將接收先前的 state 作為第一個參數,並且將更新的 props 作為第二個參數:
// 正確
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));在上面我們使用 arrow function,但它也可以適用於正常的 function:
// 正確
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});State 的更新將會被 Merge
當你呼叫 setState() 時,React 會 merge 你提供的 object 到目前的 state。
例如,你的 state 可能包含幾個單獨的變數:
  constructor(props) {
    super(props);
    this.state = {
      posts: [],      comments: []    };
  }然後你可以單獨的呼叫 setState() 更新它們:
  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts      });
    });
    fetchComments().then(response => {
      this.setState({
        comments: response.comments      });
    });
  }這個 merge 是 shallow 的,所以 this.setState({comments}) 保持 this.state.posts 的完整,但它完全取代了 this.state.comments。
向下資料流
Parent 和 child component 不會知道某個 component 是 stateful 或 stateless 的 component,而且它們不在意它是透過 function 或是 class 被定義的。
這就是 state 通常被稱為 local state 或被封裝的原因。除了擁有和可以設定它之外的任何 component 都不能訪問它。
Component 可以選擇將它的 state 做為 props 往下傳遞到它的 child component:
<FormattedDate date={this.state.date} />FormattedDate component 會在它的 props 接收到 date,但他不知道它是從 Clock 的 state 傳遞過來的,從 Clock 的 props 或者是透過手動輸入:
function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}這通常被稱作為「上至下」或「單向」的資料流。任何 state 總是由某個特定的 component 所擁有,任何從 state 得到的資料或 UI,state 只能影響在 tree「以下」的 component。
如果你想像一個 component tree 是一個 props 的瀑布,每個 component 的 state 像是一個額外的水流源頭,它在任意的某個地方而且往下流。
為了表示所有 component 真的都是被獨立的,我們可以建立一個 App component 來 render 三個 <Clock>:
function App() {
  return (
    <div>
      <Clock />      <Clock />      <Clock />    </div>
  );
}每個 Clock 設定它本身的 timer 並獨立的更新。
在 React 應用程式中,不論 component 是 stateful 或 stateless 都被視為是實作 component 的細節,它可能隨著時間而改變。你可以在 stateful component 內使用 stateless component,反之亦然。