React State

React Component 只能透過資料狀態的改變來更新 UI,而 React 元件有兩種資料來源:

  1. 透過外部傳進來元件的 Props
  2. 元件內部自己維護的 State

每當 React 偵測到 Props 或 State 有更新時,就會自動重繪整個 UI 元件。

State 的資料結構跟 Props 一樣,只是一個 JavaScript Object,而 State 只能在 Class Components 中使用。

Props 對於元件是唯讀 (read-only) 的資料,而 State 是元件可以自由讀寫的。

一般來說,如果一個資料不會用在 render() 的顯示邏輯,這資料不應該被存在 State,你可以存在其他地方,像是 class/object property 中。

React State 實作上,我們會在 Component Class 的 constructor() 中用 this.state 來初始化 State 物件;而在 Component Class 的任何方法 (methods) 中,使用 this.state 來存取 State;如果你要更新 State 的資料時,使用 React 元件提供的方法 this.setState() 來更新 State。

setState 的基本語法是:

this.setState(stateChange)

參數 stateChange 是一個 key-values Object 放你想要更新的資料。

舉個例子,如果來寫一個時鐘 Clock 元件:

  1. 你需要一個可以自我管理的 State 資料結構,來更新目前最新時間狀態資料:

    class Clock extends React.Component {
        
      // 當元件被 ReactDOM.render() 渲染到畫面時
      // React 會先執行元件的 constructor
      constructor(props) {
        super(props);
            
        // Clock 元件顯示到畫面時,需要知道目前的時間是什麼
        // 所以我們需要先初始化好 state 資料
        this.state = {date: new Date()};
      }
        
      // React call render() 拿到要顯示到 DOM 的元素內容
      render() {
        return (
          <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
          </div>
        );
      }
    }
        
    ReactDOM.render(
      <Clock />,
      document.getElementById('root')
    );
    

    點我看這個例子的結果

  2. React Component 提供了一些 Class Methods,讓你可以針對該物件在幾個特殊事件或狀態發生時做些你想處理的事情 (hook),這些 Methods 我們稱之 Lifecycle Methods

    像是當元件第一次被渲染 (render) 到頁面 (DOM) 後,React 稱這事件/動作叫做 mounting (掛載),當 mounting 發生時,React 會執行相對應的 Lifecycle method componentDidMount();當元件準備從 DOM 中被移除之前,這事件稱作 unmounting (卸載),React 會執行相對應的 Lifecycle method componentWillUnmount()

    我們再來繼續改寫 Clock 元件:

    class Clock extends React.Component {
      constructor(props) {
        super(props);
        this.state = {date: new Date()};
      }
        
      // 當元件被顯示到 DOM 後,React 會 call componentDidMount()
      componentDidMount() {
        // 我們啟動一個 timer 每秒鐘會自動更新時間
        this.timerID = setInterval(
          () => this.tick(),
          1000
        );
      }
    
      // 當元件要從 DOM 中被移除 (remove) 前
      // React 會 call componentWillUnmount()
      componentWillUnmount() {
        // 做些資源清理的動作
        // 清掉 timer
        clearInterval(this.timerID);
      }
        
      tick() {
        // 我們用 setState() 來更新 State 資料
        // React 會偵測到 State 的更新,call render() 重繪畫面
        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')
    );
    

    點我看這個例子的結果

this.setState()

再來進一步說明更新 State 時的一些細節。

除了在 constructor() 裡面,否則 React 禁止你直接更改 State 物件:

// 在非 constructor() 的方法中
this.state.comment = 'Hello'; // 錯誤

不然 React 不會知道 State 有被更新。

在 constructor() 以外的地方,要更新 State 只能透過 setState() 方法:

// 正確更新 State 的方法
this.setState({comment: 'Hello'});

setState(stateChange) 會和當前的 State 合併只做部分更新

你在 setState() 不用將完整的 State (全部的 key-value pairs) 都丟進去更新,你只需要傳入你想要更新的 key-value pairs 就好,React 會自動將你傳入的 key-value pairs 和當前 State 的 key-value pairs 合併 (merge),新的 key-value paris 會取代掉舊值。

例如:

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

你可以在不同的 setState() function calls 中分別更新 posts 和 comments 的值:

componentDidMount() {
  // 先來更新 posts 資料
  fetchPosts().then(response => {
    // 只會更新 posts 的值,不會動到 comments 目前的值
    this.setState({
      posts: response.posts
    });
  });

  // 再來更新 comments 資料
  fetchComments().then(response => {
    // 只會更新 comments 的值,不會動到 posts 目前的值
    this.setState({
      comments: response.comments
    });
  });
}

State 和 Props 是非同步 (Asynchronous) 更新的

State 和 Props 的更新是非同步的 (Asynchronous),因為 React 為了效能的考量,會將你好幾次的 setState() calls 統一合成一次做 batch 更新。

意思就是說當你執行完 setState() 更新 State 後,接著下一行 code,你馬上存取 this.state 不一定會拿到最新的值。

因為資料更新是非同步的特性,也不能依賴拿 this.statethis.props 做計算來得到一個新的 State 值來 setState()。

例如這個例子是錯誤的實作:

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

setState 有另一個語法:

this.setState( (prevState, props) => stateChange )

setState 可以接受一個 function 當作參數,該 function 的參數 prevState 確保你拿到的是當前最新的 State,而參數 props 是元件當前的 props,在 function 返回一個你要更新的 State (一個 key-values Object)。

上面錯誤的例子,修正後正確的寫法應該是:

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

如果你看不懂 => 這語法,那是 ES6 的 Arrow Function

setState(stateChange, callback)

也因為 State 的更新是非同步的,setState 還有一個 optional 的第二個參數 callback,callback 是一個 function,用途是當 State 確定已經被更新後,React 就會 call 這個 callback function,讓你可以在 callback 中做些你需要確定 State 已經被更新後才能做的事。

運用的例子像是:

changeInput(event) {
  this.setState({text: event.target.value}, function() {
    this.validateInput();
  });
}

validateInput() {
  if (this.state.text.length === 0) {
    this.setState({inputError: 'Input cannot be blank'});
  }
}

Stateful / Stateless Component

什麼是 Stateful Component (有狀態元件) 和 Stateless Component (無狀態元件)?

所謂的 Stateless Component 是指單純負責純靜態顯示的元件,元件自身不維護自己 local 的狀態 (State),不涉及到狀態的更新 (Lifecycle),基本上元件中只有 propsrender

Functional Component 就是標準的 Stateless Component。

而 Stateful Component 則是元件內部有維護自身狀態 (State) 的改變,Stateful Component 通常會伴隨事件處理或在不同生命週期下觸發狀態的更新。

在 React 中,不管是 Parent Components 或 Child Components 都無法得知某個元件是 Stateful 或是 Stateless,所以會說 State 是 local 或 encapsulated (封裝) 的。

單向資料流 (Unidirectional Data Flow)

React 的資料架構設計概念是採單向資料流 (top-down or unidirectional data flow),資料的流向只能從父元件 State/Props 傳入子元件 Props,而資料改變時會牽動 UI 自動改變,每當 React 偵測到 props 或 state 有更新時,就會自動重繪整個 UI 元件。

想像一個元件樹,其間的資料傳遞,就像是一個 Props 的瀑布流,從上 (Root/Parents) 流到下 (Children),而每個元件自己的 State 就像是另外注入新的水流,但一樣是往下流去。