所有文章

你可能不需要 Derived State

何時使用 derived state

getDerivedStateFromProps 的意義僅在使組件依其 props 的變化來改變其內部 state。Derived state 不應頻繁使用,須遵循以下原則:

  • 如果你使用 derived state 以 memoize 一些祇基於當前 props 的計算,你就不需要 derived state。
  • 如果你無條件更新 derived state 或每當 props 和 state 不匹配時更新之,你的組件可能會過於頻繁地重置其 state。

使用 derived state 的常見 bugs

對於組件有以下二類:

  • controlled 組件:數據以 props 傳入,父組件得以控制數據
  • uncontrolled 組件:數據僅存於內部 state,父組件不能直接改變之

Derived state 的最常見錯誤是混淆此兩者。若一 derived state 值也能被 setState 所更新,該數據就不止有一個 source of truth。

反模式:無條件複製 props 至 state

一個常見誤解是 getDerivedStateFromPropscomponentWillReceiveProps 祇在 props 變更時調用。事實是每當父組件重渲染時,這些 lifecycles 都會觸發,而不論 props 是否有變。因此,在這些 lifecycles 中無條件地覆蓋 state 並不安全,會造成 state 更新丟失。

一個將 prop 映射至 state 的 EmailInput 組件:

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  componentWillReceiveProps(nextProps) {
    // 這會擦除任何本地狀態更新!
    // 別這麼做。
    this.setState({ email: nextProps.email });
  }
}

每當其父組件重渲染時,往 <input> 裏鍵入的內容就會丟失(見例)。

加入 shouldComponentUpdate 來讓組件祇在 email prop 變更時才重渲染,可以解決這個問題。但實際情況中,組件經常接收多個 props;另外的 prop 變更也會引致重渲染。shouldComponentUpdate 應祇用於優化性能,而非用以糾正 derived state。

反模式:props 變更時抹除 state

componentWillReceiveProps(nextProps) {
    // 每當 `props.email` 變化時,即更新狀態。
    if (nextProps.email !== this.props.email) {
      this.setState({
        email: nextProps.email
      });
    }
  }

現在組件祇會在 props 真正變更時,才抹除鍵入的內容。但在一些邊緣需求的用例下,會引致 bugs(見例)。

Preferred solutions

建議:fully controlled 組件

從組件完全移除 state——若 email 地址祇以 prop 形式存在,則不必擔心與 state 衝突。甚至可以把 EmailInput 轉換成一個輕量的函數組件:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

建議:帶 key 的 fully uncontrolled 組件

讓組件完全擁有「draft」email 地址,但仍可接收一個 prop 作為初始值,唯忽略其後續變更。

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}

(就上例的密碼管理器而言)為其加入一個 key 屬性。當 key 變化時,React 會新建一個組件實例,而非更新當前的組件實例。

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

每當 ID 有變,EmailInput 會被重建且其 state 被重置(見例)。你祇須為整個 form 配置一個 key,每當 key 變化時,form 內的所有組件都會被重建並重置 state。

在多數情況下,這是處理需要重置的 state 的最好方式。

如果出於某些原因 key 沒有用(可能組件的初始化非常昂貴),一個可能但笨拙的方法是在 getDerivedStateFromProps 中監測 userID 的變動:

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };
  
  static getDerivedStateFromProps(props, state) {
    // Any time the current user changes,
    // Reset any parts of state that are tied to that user.
    // In this simple example, that's just the email.
    if (props.userID !== state.prevPropsUserID) {
      return {
        email: props.defaultEmail,
        prevPropsUserID: props.userID
      };
    }
    return null;
  }
  
  // ...
}

完 :)

發佈於 28 Jul 2018

慎獨|純亦不已
Jason Lam on Twitter