目次
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. コンポーネントのライフサイクル
この記事のソースコード
ソースコードはGitHubで公開しています。
今回のコミット一覧↓
コンポーネントのライフサイクル
ライフサイクル、という言葉はプログラミングの世界でよく使われますが、簡単に言うと生まれてから死ぬまでに起こるイベントなどのことです。
今回のコンポーネントのライフサイクルですが、簡単に言うとコンポーネントが画面に表示されてから画面からなくなるまでの間に起こるイベントのことを言います。
Reactのコンポーネントのライフサイクルは下図のようになっています。
Mountingが表示されるとき、Updatingは表示の内容が変わる時、Unmountingが画面から消える時、と覚えてください。
例えば、
表示する前に、APIからデータを取って表示する。
表示が終わったらデータを削除する、監視を終了する。
などのようなことはとても良く行われるのですが、プログラム上、画面への表示や画面から消えるのは明示的にコードを書くわけではない(例えば show() みたいなメソッドで表示するわけではない)ので、Reactが起こしてくれたイベントをキャッチしてそこに始めて表示するときに実行したいコードを書くわけですね。
ライフサイクル用のメソッド
画像の一番下に記載されているメソッドがライフサイクルのメソッドです。
簡単に言うと、
- コンポーネントが初期化されるときに呼び出されるのが
componentDidMount
- コンポーネントの更新ごとに呼び出されるのが
componentDidUpdate
- コンポーネントが削除されるときに呼び出されるのが
componentWillUnmount
になります。
これらのメソッドは Class Components
でのメソッドになります。
Class Components
ではこれらのメソッドがそのまま用意されていて、簡単に利用することができます。
Funciton Component
では、これらの3つのメソッドは useEffect
という一つのメソッドで表現されます。
実際にコードを書いてみる
実際に、 App.js
が言語データを取得するのを、初期化時に変更してみましょう。
APIでの取得をモックするために、 const/languages.js
に function
を追加してみましょう。
const
じゃなくなってしまいましたが、一旦無視します。
const LANGUAGES = [
'JavaScript',
'C++',
'Ruby',
'Java',
'PHP',
'Go'
];
export const getLanguages = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(LANGUAGES);
}, 1000);
});
};
getLanguages
という関数を追加しました。
これは実行されて1秒経つと LANGUAGES
を返すという関数になります。
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秒後に言語のリストが表示されるようになりました。
コード解説
import { useState, useEffect } from 'react';
今回は、 react
から useEffect
のインポートを追加しています。
useEffect(() => { // 追加
console.log('App.js:useEffect');
fetchLanguages(); // APIから取得するイメージ
}, []);
useEffect
は、2つの引数を取ります。
1つ目の引数は関数で、2つ目の引数は依存する変数の配列です。
useEffect
を利用すると、先程説明した componentDidMount
と componentDidUpdate
のどちらの場合でも、第一引数の関数が呼び出されます。つまり、初期化時、および状態の変更時に呼び出されます。
ただし、第2引数が与えられるとその値が変更されたときだけ呼び出されるようになります。
ということは、第2引数に空の配列を指定すると componentDidMount
と同じように動作させることができます。
useEffect(()=> {
// 初期化時、更新時(状態変更時など)に常に呼ばれるが、
}, [count]) // これを指定すると count の変更時のみ実行されるようになる
ちょっと触っても理解できないかもしれないので、少し useEffect
で遊んでみましょう。
useEffectで遊んでみる
useEffect
の第2引数を取り除いてみると…
useEffect(() => {
console.log('App.js:useEffect');
fetchLanguages(); // APIから取得するイメージ
}); // <= ここから配列を取り除いた
全体のコードは下記です。
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引数の関数が呼び出されるはずですね。
実際に見ていきましょう。
上図のように、タブを切り替えるだけでも useEffect
の第1引数の関数が呼び出されていることがわかります。
上図の動作を解説すると、
- 初期化時
- 1秒後に
setLangs
実行による更新 - フォームを押して
setTab
実行による更新 - リストを押して
setTab
実行による更新 - フォームを押して
setTab
実行による更新
の5回の呼び出しがありました。
もう少し遊んでみます。
useEffect
の第2引数に langs
を入れてみる。
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;
今度はこうなります。
今度はタブの切り替えでは実行されなくなったのがわかります。
さらにもう一つやってみましょう。
useEffect
の第2引数に tab
を入れてみる。
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;
今度は更新時に 2回実行されるところが実行されなくなり、タブの変更での実行だけになりました。
そろそろuseEffect
の動きがわかってきたでしょうか。
ComponentWillUnmount
useEffect
を使って componentDidMount
と componentDidUpdate
、つまり初期化と更新時の挙動を見ていきました。
しかし、最初に紹介したライフサイクルは3つありました。
最後の一つ、 componentWillUnmount
つまりコンポーネント削除時のライフサイクルが残っています。
Function Components
では、 useEffect
の第一引数の返り値が componentWillUnmount
と同等の動きをします。
実際にコードを見てみるとわかりやすいです。
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
で関数を返しているのがわかるでしょうか。
これがコンポーネント削除時に実行されます。
実際の動きを見てみましょう。
タブが変わるとコンソールにログが出ていることがわかります。
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(<コンポーネントのファイル名>)
を入れてみましょう。
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;
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>
)
}
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>
)
};
表示がかわるたびに App.js
と 子コンポーネントの console.warn
が呼ばれていることがわかります。
Function Component
はただの function
なので、表示されるたびに呼び出されているんですね。
面白いです。
コード整理
一旦今日記載したコードから今後不要と思われるコードを消していきます。
export const List = ({ langs }) => {
return (
<div>
{
langs.map((lang, index) => {
return <div key={index}>{ lang }</div>
})
}
</div>
)
};
List.js
をもとに戻しました。
Class Components
Class Components
では、ライフサイクルのメソッドが用意されており、それをクラスで定義することで利用できます。
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
も同様にメソッドを定義することで記載できます。
おわりに
今回はライフサイクルを学んでいきました。
これでライフサイクルまでちゃんとわかってくると、もう大抵のことができるようになったと言っても良いかもしれません。