11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React入門 未経験から1週間でReactをマスターする #06. コンポーネントのライフサイクル

Posted at

目次

1. Reactの新規プロジェクトの立ち上げ
2. コンポーネントのプロパティ(props)とステート(state)
[3. Class Components と Function Components] (https://qiita.com/yassun-youtube/items/2ed8601e4fa477726705)
4. 条件分岐 (if) と繰り返し (loop)
5. フォームと親子間のデータのやり取り
6. コンポーネントのライフサイクル←今ここ
7. スタイル (準備中)
8. Higher-Order Component (準備中)
9. Portalを利用したモーダル (準備中)
10. refによるエレメントの取得 (準備中)
11. Contextを利用したテーマの変更 (準備中)

今回の学習内容

今回は、

  • Reactのコンポーネントのライフサイクルを理解する
  • ライフサイクルごとのメソッドの利用方法を理解する

をやっていきます。

YouTubeでの解説動画

YouTubeでも解説しています。
動画で確認したい方はこちらもどうぞ。
【YouTube動画】 未経験から1週間でをマスターするReact入門 #06. コンポーネントのライフサイクル
未経験から1週間でをマスターするReact入門 #06. コンポーネントのライフサイクル

この記事のソースコード

ソースコードはGitHubで公開しています。

今回のコミット一覧↓

スクリーンショット 2020-12-30 16.20.55.png

コンポーネントのライフサイクル

ライフサイクル、という言葉はプログラミングの世界でよく使われますが、簡単に言うと生まれてから死ぬまでに起こるイベントなどのことです。
今回のコンポーネントのライフサイクルですが、簡単に言うとコンポーネントが画面に表示されてから画面からなくなるまでの間に起こるイベントのことを言います。

Reactのコンポーネントのライフサイクルは下図のようになっています。

_2020-11-02_0.13.36.png

Mountingが表示されるとき、Updatingは表示の内容が変わる時、Unmountingが画面から消える時、と覚えてください。

例えば、
表示する前に、APIからデータを取って表示する。
表示が終わったらデータを削除する、監視を終了する。

などのようなことはとても良く行われるのですが、プログラム上、画面への表示や画面から消えるのは明示的にコードを書くわけではない(例えば show() みたいなメソッドで表示するわけではない)ので、Reactが起こしてくれたイベントをキャッチしてそこに始めて表示するときに実行したいコードを書くわけですね。

ライフサイクル用のメソッド

画像の一番下に記載されているメソッドがライフサイクルのメソッドです。

簡単に言うと、

  • コンポーネントが初期化されるときに呼び出されるのが componentDidMount
  • コンポーネントの更新ごとに呼び出されるのが componentDidUpdate
  • コンポーネントが削除されるときに呼び出されるのが componentWillUnmount

になります。

これらのメソッドは Class Components でのメソッドになります。

Class Components ではこれらのメソッドがそのまま用意されていて、簡単に利用することができます。
Funciton Component では、これらの3つのメソッドは useEffect という一つのメソッドで表現されます。

実際にコードを書いてみる

実際に、 App.js が言語データを取得するのを、初期化時に変更してみましょう。

APIでの取得をモックするために、 const/languages.jsfunction を追加してみましょう。
const じゃなくなってしまいましたが、一旦無視します。

const/languages.js
const LANGUAGES = [
  'JavaScript',
  'C++',
  'Ruby',
  'Java',
  'PHP',
  'Go'
];

export const getLanguages = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(LANGUAGES);
    }, 1000);
  });
};

getLanguages という関数を追加しました。

これは実行されて1秒経つと LANGUAGES を返すという関数になります。

src/App.js
import { useState, useEffect } from 'react'; // 変更
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState([]);

  useEffect(() => { // 追加
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }, []);

  const fetchLanguages = async () =>{ // 追加
    const languages = await getLanguages();
    setLangs(languages);
  };

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list');
  };

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/>
      }
    </div>
  );
}

export default App;

これで、表示されて下図のように1秒後に言語のリストが表示されるようになりました。

61f463c3e17d5d3a7ea133eb7dacea48.gif

コード解説

import { useState, useEffect } from 'react';

今回は、 react から useEffect のインポートを追加しています。

  useEffect(() => { // 追加
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }, []);

useEffect は、2つの引数を取ります。

1つ目の引数は関数で、2つ目の引数は依存する変数の配列です。

useEffect を利用すると、先程説明した componentDidMountcomponentDidUpdate のどちらの場合でも、第一引数の関数が呼び出されます。つまり、初期化時、および状態の変更時に呼び出されます。

ただし、第2引数が与えられるとその値が変更されたときだけ呼び出されるようになります。

ということは、第2引数に空の配列を指定すると componentDidMount と同じように動作させることができます。

useEffect(()=> {
  // 初期化時、更新時(状態変更時など)に常に呼ばれるが、
}, [count]) // これを指定すると count の変更時のみ実行されるようになる

ちょっと触っても理解できないかもしれないので、少し useEffectで遊んでみましょう。

useEffectで遊んでみる

useEffect の第2引数を取り除いてみると…

src/App.js
 useEffect(() => {
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }); // <= ここから配列を取り除いた

全体のコードは下記です。

src/App.js
import { useState, useEffect } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState([]);

  useEffect(() => {
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }); // <= ここから配列を取り除いた

  const fetchLanguages = async () =>{
    const languages = await getLanguages();
    setLangs(languages);
  };

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list');
  };

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/>
      }
    </div>
  );
}

export default App;

こうすると、初期化時と更新時に常に useEffect の第1引数の関数が呼び出されるはずですね。

実際に見ていきましょう。

40d3cb1ec216b9f4fddcb22e3374878c.gif

上図のように、タブを切り替えるだけでも useEffect の第1引数の関数が呼び出されていることがわかります。

上図の動作を解説すると、

  1. 初期化時
  2. 1秒後に setLangs 実行による更新
  3. フォームを押して setTab 実行による更新
  4. リストを押して setTab 実行による更新
  5. フォームを押して setTab 実行による更新

の5回の呼び出しがありました。

もう少し遊んでみます。

useEffect の第2引数に langs を入れてみる。

src/App.js
import { useState, useEffect } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState([]);

  useEffect(() => {
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }, [langs]); // ここを [langs] に変更

  const fetchLanguages = async () =>{
    const languages = await getLanguages();
    setLangs(languages);
  };

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list');
  };

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/>
      }
    </div>
  );
}

export default App;

今度はこうなります。

2c74734a6721d0fe8f21272b809be3c8.gif

今度はタブの切り替えでは実行されなくなったのがわかります。

さらにもう一つやってみましょう。

useEffect の第2引数に tab を入れてみる。

src/App.js
import { useState, useEffect } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState([]);

  useEffect(() => {
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }, [tab]); // ここを [tab] に変更

  const fetchLanguages = async () =>{
    const languages = await getLanguages();
    setLangs(languages);
  };

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list');
  };

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/>
      }
    </div>
  );
}

export default App;

505bdac4a8debd0b372ccbce2a5ef88f.gif

今度は更新時に 2回実行されるところが実行されなくなり、タブの変更での実行だけになりました。

そろそろuseEffect の動きがわかってきたでしょうか。

ComponentWillUnmount

useEffect を使って componentDidMountcomponentDidUpdate 、つまり初期化と更新時の挙動を見ていきました。

しかし、最初に紹介したライフサイクルは3つありました。

最後の一つ、 componentWillUnmount つまりコンポーネント削除時のライフサイクルが残っています。

Function Componentsでは、 useEffect の第一引数の返り値が componentWillUnmount と同等の動きをします。

実際にコードを見てみるとわかりやすいです。

List.js にコードを追加してみます。

src/List.js
import { useEffect } from 'react'; // 追加

export const List = ({ langs }) => {
  useEffect(() => { // 追加
    console.log('List.js:useEffect');

    return () => { // ここが componentWillUnmount
      console.log('List.js:useEffect:unmount')
    }
  });

  return (
    <div>
      {
        langs.map((lang, index) => {
          return <div key={index}>{ lang }</div>
        })
      }
    </div>
  )
};

全体のコードはこちらですが、 useEffect に集中して見てみましょう。

  useEffect(() => { // 追加
    console.log('List.js:useEffect');
  
	  return () => { // ここが componentWillUnmount
      console.log('List.js:useEffect:unmount')
    }
  });

useEffect の第1引数の関数の return で関数を返しているのがわかるでしょうか。

これがコンポーネント削除時に実行されます。

実際の動きを見てみましょう。

6209c551c854f15b6a8f2f10399e29c5.gif

タブが変わるとコンソールにログが出ていることがわかります。

List.js は、タブの状態で表示されたり表示されなくなったりします。

この表示されなくなったタイミング(フォームが押されたタイミング)で List.js:useEffect:unmountという文言が表示されていますね。

今日やったこと1

コンポーネントのライフサイクルを理解する

初期化時 => componentDidMount, useEffect
更新時(new props, setState, forceUpdate) => componentDidUpdate, useEffect
終了時 => componentWillUnmount, useEffect(第1引数の返り値)

function componentsでは useEffect

useEffect(() => {
  // mounting, updating時に呼ばれる
})
useEffect(() => {
  // mounting時に呼ばれる
}, [])
useEffect(() => {
  // mounting, langsの変更時に呼ばれる
}, [langs])
useEffect(() => {
  // mounting, updating時に呼ばれる
  return () => {
    // unmounting時に呼ばれる
  }
})

class componentsではそれぞれのメソッド

class Test extends React.Component {
  componentDidMount() {
    // mounting時に呼ばれる
  }
  componentDidUpdate() {
    // updating時に呼ばれる
  }
  componentWillUnmount() {
    // unmounting時に呼ばれる
  }
}

もっとconsoleで遊んでみる

各コンポーネントの最初に console.log(<コンポーネントのファイル名>) を入れてみましょう。

src/App.js
import { useState, useEffect } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

function App() {
  console.warn('App.js'); // ここを追加
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState([]);

  useEffect(() => {
    console.log('App.js:useEffect');
    fetchLanguages(); // APIから取得するイメージ
  }, []);

  const fetchLanguages = async () =>{
    const languages = await getLanguages();
    setLangs(languages);
  };

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list');
  };

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/>
      }
    </div>
  );
}

export default App;
src/Form.js
import { useState } from 'react';

export const Form = ({ onAddLang }) => {
  console.warn('Form.js'); // ここを追加
  const [text, setText] = useState('');

  const submitForm = (e) => {
    e.preventDefault();
    onAddLang(text);
  }

  return (
    <div>
      <h4>新しい言語の追加</h4>
      <form onSubmit={submitForm}>
        <div>
          <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
        </div>
        <div>
          <button>追加</button>
        </div>
      </form>
    </div>
  )
}
src/List.js
import { useEffect } from 'react';

export const List = ({ langs }) => {
  console.warn('List.js'); // ここを追加

  useEffect(() => {
    console.log('List.js:useEffect');
    return () => {
      console.log('List.js:useEffect:unmount')
    }
  });

  return (
    <div>
      {
        langs.map((lang, index) => {
          return <div key={index}>{ lang }</div>
        })
      }
    </div>
  )
};

コンソールがどのようになるか見てみましょう。
ab598cb63d6823271010a8bfda484ee7.gif

表示がかわるたびに App.js と 子コンポーネントの console.warn が呼ばれていることがわかります。

Function Component はただの function なので、表示されるたびに呼び出されているんですね。

面白いです。

コード整理

一旦今日記載したコードから今後不要と思われるコードを消していきます。

src/List.js
export const List = ({ langs }) => {
  return (
    <div>
      {
        langs.map((lang, index) => {
          return <div key={index}>{ lang }</div>
        })
      }
    </div>
  )
};

List.js をもとに戻しました。

Class Components

Class Components では、ライフサイクルのメソッドが用意されており、それをクラスで定義することで利用できます。

src/App.js
import React from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { getLanguages } from "./const/languages";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      tab: 'list',
      langs: [], // 変更
    };
  }
  componentDidMount() { // 追加
    this.fetchLanguages();
  }
  async fetchLanguages() { // 追加
    const langs = await getLanguages();
    this.setState({ langs })
  }
  addLang(lang) {
    this.setState({
      langs: [...this.state.langs, lang],
      tab: 'list',
    });
  }
  render() {
    const { tab, langs } = this.state;
    return (
      <div>
        <header>
          <ul>
            <li onClick={() => this.setState({ tab: 'list' })}>リスト</li>
            <li onClick={() => this.setState({ tab: 'form' })}>フォーム</li>
          </ul>
        </header>
        <hr />
        {
          tab === 'list' ? <List langs={langs} /> : <Form onAddLang={(lang) => this.addLang(lang)}/>
        }
      </div>
    )
  }
}

export default App;

更新時に呼ばれる componentDidUpdate や削除時に呼ばれる componentWillUnmount も同様にメソッドを定義することで記載できます。

おわりに

今回はライフサイクルを学んでいきました。
これでライフサイクルまでちゃんとわかってくると、もう大抵のことができるようになったと言っても良いかもしれません。

11
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?