元営業WEBエンジニアのアプリ開発日記

営業出身のWEB系エンジニアが気になったものから作ってはメモを残してくブログ

React Lifecycleを一通り検証して見る

概要

とりあえずcomponentDidMount()使っとけばいいでしょ。
てな温度感で使ってて良くないなと思ったのでちゃんとLifecycle全部使ってみた。

公式での説明

★を使うのはレアケースらしい。

mounting時

  • constructor(props)
    • stateの初期化、メソッドのbindをおこなう
  • static getDerivedStateFromProps(props, state) ★
    • propsとstateをもとにstateを更新!
    • stateを更新しない場合nullを返却
  • render()
  • componentDidMount()
    • component mount直後に呼び出される
    • リモートからデータをロードするなどに最適な場所

updating時

  • static getDerivedStateFromProps(props, state) ★
  • shouldComponentUpdate(nextProps, nextState) ★
    • 新しいprops, stateをもとにcomponentをupdateするかどうか判断
    • true返却でアップデート、false返却でアップデートしない
  • render()
  • getSnapshotBeforeUpdate(prevProps, prevState) ★
    • 前回のpropsとstateをもとにsnapshotを作成
    • 作成したsnapshotはcomponentDidUpdateで受け取ることができる
    • 公式では画面スクロールで使えるとかどうとか言ってた
  • componentDidUpdate(prevProps, prevState, snapshot)
    • component update直後に呼び出される
    • さっき記載したsnapshotを受け取れる!!

Unmounting時

  • componentWillUnmount()
    • component unmount直前に呼び出される
    • クリーンナップ作業をここでやるらしい

Error Handling時

  • static getDerivedStateFromError(error)
    • error発生したら実行される
    • stateのエラー更新して、エラー画面表示するとか
  • componentDidCatch(error, info)
    • エラー取得してエラー記録サービスに連携したりできる

実際の実験

実際に実験してみたんでメモしとく

import React from "react";
import Lifecycle from "./Lifecycle";

interface State {
  id: string;
}
class App extends React.Component<{}, State>{
  constructor(props: {}) {
    super(props);
    this.state = {
      id: "",
    };
    this.handleInput = this.handleInput.bind(this);
  }
  public render() {
    return (
      <Lifecycle id={this.state.id} handleInput={this.handleInput} />
    );
  }
  private handleInput(event: React.ChangeEvent<HTMLInputElement>) {
    const { value } = event.target;
    this.setState({
      id: value,
    });
  }
}

export default App;
import React from "react";

interface Props {
  id: string;
  handleInput: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

interface State {
  status: string;
  hasError: boolean;
}

class Lifecyle extends React.Component<Props, State>{
  constructor(props: Props) {
    super(props);
    console.log("constructor called");
    this.state = {
      status: "",
      hasError: false
    }
  }

  static getDerivedStateFromProps(props: Props, state: State) {
    console.log("getDerivedStateFromProps called");
    if (props.id === "target") {
      console.log("state is changed");
      return {
        status: "state changed",
      };
    }
    console.log("state is not changed");
    return null;
  }

  public shouldComponentUpdate(nextProps: Props, nextState: State) {
    console.log("shouldComponentUpdate called");
    if (this.props.id !== nextProps.id) {
      console.log("current prop id !== next prop id let's update component!");
      return true;
    }
    console.log("current prop id === next prop id don't update component!");
    return false;
  }

  static getDerivedStateFromError(error: Error) {
    return {
      hasError: true,
    }
  }

  public render() {
    const { id, handleInput } = this.props;
    const {status} = this.state;
    if (this.state.hasError) {
      return (
        <div>error happened</div>
      );
    } else {
      return (
        <>
          <input type="text" value={id} onChange={handleInput} />
          <div>{status}</div>
        </>
      );
    }
  }

  /**
   * componentがmountされた直後に実行
   * リモートからデータロードするときに良い
   * ここでsetStateすべきではない。2回renderして遅くなる
   */
  public componentDidMount() {
    console.log("componentDidMount called");
  }

  public getSnapshotBeforeUpdate(prevProps: Props, prevState: State) {
    console.log("getSnapshotBeforeUpdate called");
    if (prevProps.id === "target") {
      console.log("prevProps.id === target carry snapshot to update component!");
      return true;
    }
    console.log("prevProps.id !== target don't carry snapshot to update component!");
    return null;
  }

  /**
   * componentがアップデートされた直後に実行
   * 前回の引数と今回の引数を比較して処理を判定
   * setStateするときは分岐に入れないと無限ループ
   * getSnapshotBeforeUpdate()がある場合snapshot引数が有効
   */
  public componentDidUpdate(prevProps: Props, prevState: State, snapshot: boolean) {
    console.log("componentDidUpdate called");
    if (this.props.id !== prevProps.id) {
      console.log("prev prop id !== curren prop id do something after update component");
    } else if (snapshot) {
      console.log("true snapshot is carried");
    }
  }

  /**
   * componentがunmountされる直前に実行
   * クリーンアップ作業を一通り実行
   * もうコンポーネントもいなくなるのでsetStateここで呼ばないで
   */
  public componentWillUnmount() {
    console.log("componentWillUnmount called");
  }

  public componentDidCatch(error: any, info: any) {
    console.log("componentDidCatch called");
  }
}

export default Lifecyle;

まとめ

基本的にはレアケース使わずシンプルな構成に保ちたいのだ