ライフサイクルの代わりにReact HooksのuseEffectを使おう
目次
useEffectの使い方とAPI
はじめに
こんにちは。Kennyです。今回はReact.jsのHooksであるuseEffectを解説していきたいと思います。クラスコンポーネントの機能からReactの動作が根本的に変わるようなものではありませんが、クラスコンポーネント内でフックを使うことはできませんので、ファンクショナルコンポーネントで試してください。React.jsのバージョンは16.80より実装可能となっております。
前提
開発環境
macOS Catalina
MacBook Pro (15inch, 2019)
npm version 6.14.4
nodebrew use v14.0.0(node)
yarn 1.22.4
React 16.13.1
React-Dom 16.13.1
useEffectを使ってみる前に
useEffectの実行タイミングの理解と確認
useEffectの意味ですが、よくわからないですよね。一体何のことを言っているのか掴みづらい印象があるかと思います。
effectとは「作用する」や「効果」と言った意味ですが副作用と言った意味として説明されます。
副作用とは実感的には処理を目的とする作用(効果)に伴って起こる別の作用のことです。
しかし有害な意味で使われることもあると思いますが、useEffectは実際のところ、ReactにおけるcomponentDidMount,componentDidUpdate,componentWillUnmount(ライフサイクルフック)がありますが、実際には違いがあるもののそれらがまとまったものだと考えることができます。
componentDidMountの実行に続いてcomponentDidUpdateが実行されますが、これだと二つの処理を記述することになります。例えば、同じstateを更新しているのに別の処理に分ける必要があると言った場合です。
この二つの処理を一つにすることができるのがuseEffectです。Reactはレンダーの後に何かの処理をしなければならないといけない、ということをHookから伝えられ、Reactはそれを覚えており、DOMの更新の後にそれを呼び出します。
useEffect内で起こる処理はまさに副作用のようにレンダーの後に呼び出されることになります。ですが後でカスタマイズについても説明します。useEffectは常に、継続的にレンダリングの後に呼び出されるものだということです。
参考ドキュメント React Hooks https://ja.reactjs.org/docs/hooks-effect.html
ではuseEffectの実行タイミングを確かめてみましょう。JSX内で実行される関数と比べてみましょう。
useEffectのインポート
import React, { useEffect } from 'react'
React HooksのuseEffectを使用するのにプラグインやモジュールを使用する必要はありません。使用するにはversionが16.8以上である必要があります。
useEffectの実行タイミングを確かめる
useEffectとrender関数の実行結果をコンソールで見てみます。
JSXのレンダリングの後にuseEffectが実行されていることがわかります。
import React, { useEffect } from 'react'
const App = () => {
const render = () => {
console.log('render関数が実行されました。')
// return '.'
}
useEffect(() => {
console.log('useEffectが実行されました。')
})
return (
<div className="App">
Hello
<div className="render-container">
{render()}
</div>
</div>
);
}
export default App;
実行タイミングを確かめてみてください。レンダリングの後に必ず呼ばれます。
では次にuseStateでstateを用意してuseEffectとの関係を探ります。
stateの更新とuseEffectの関係を確かめよう
useStateをインポートしましょう
import React, { useState, useEffect } from 'react'
細かいuseStateの使い方は以前に記事にしていますのでまだ使ったことがないよという方はこちらです。useStateは前回のstateの値を利用して使用する記述方法やfor文を使った記述に加え、stateがオブジェクトの時の記述方法も合わせて解説しております。
useStateとuseEffectの関係を見ていくのでrender関数は一旦コメントアウトして下記のようなコードで試していきます。
import React, {useState, useEffect} from 'react';
import './App.css';
const App = () => {
const [name, setName] = useState({firstName: ''})
useEffect(() => {
console.log('useEffectが実行されました。')
})
return (
<div className="App">
useStateとuseEffect
<div className="render-container">
<input
type="text"
value={name.firstName}
onChange={(e) => setName({firstName: e.target.value})}/>
</div>
</div>
);
}
export default App;
上記ではstateがfirstNameのオブジェクトで、初期値が空で定義されています。
inputタグにstateの更新を感知するonChangeメソッドがあり、セッター関数であるsetNameにinputの値が入ってきます。
inputの値(state)が更新されるたびにレンダリングが走ることになるので、useEffect内の関数はレンダリングの後に必ず呼ばれることになります。
このことから、副作用的な処理として扱いたくないstateの更新がある場合、useEffectにはカスタマイズが用意されています。
と言いますか、ほとんどの場合において不用意な再レンダリングを避ける為にカスタマイズが使用されることになります。
useEffectのカスタマイズ
不用意な再レンダリングを避ける為のカスタマイズですが、具体的にはuseEffectの第二引数を使用します。
useEffect(() => {
console.log('useEffectが実行されました。')
}, [])
上記のように第二引数に空の配列を渡すことで次回からのレンダリングに引っ張られることがなくなります。
依存する値がrender後も変わらないためにuseEffectをスキップしているのです。
ではstateの値を出力して確かめてみましょう。
クリーンアップしたいstateを第二引数に渡す
レンダリングが走るたびに副作用的にuseEffectが連動して処理が走るのを制御できたとしても、だったら何のためのuseEffectなのかと思う方もいらっしゃるのではないでしょうか。
複数のstateがある場合など、特定のstateにおいて制御を振り分けることが可能となりますので解説していきます。
const [firstName, setFirstName] = useState({firstName: ''})
const [lastName, setLastName] = useState({lastName: ''})
上記のようにstateやファイルを更新してください。
ここで行うことは
- stateを複数にして、そのstate用のinput要素を追加する
- useEffectを二つ用意する
- useEffectの第二引数に[firstName], [lastName]をそれぞれ代入する
全文は下記のようになります。
import React, { useState, useEffect } from 'react';
import './App.css';
const App = () => {
const [firstName, setFirstName] = useState({firstName: ''})
const [lastName, setLastName] = useState({lastName: ''})
useEffect(() => {
console.log(`${firstName.firstName}が更新されました`)
}, [firstName])
useEffect(() => {
console.log(`${lastName.lastName}が更新されました`)
}, [lastName])
return (
<div className="App">
useStateとuseEffect
<div className="render-container">
<input
type="text"
value={ firstName.firstName }
onChange={(e) => setFirstName({ firstName: e.target.value}) }/>
<input
type="text"
value={ lastName.lastName }
onChange={(e) => setLastName({ lastName: e.target.value })}
/>
</div>
{ firstName.firstName }
{ lastName.lastName }
</div>
);
}
export default App;
上記のように第二引数にstateを渡すことでそのstateが更新されてレンダリングされた時のみuseEffectの副作用の処理が走るようにすることができます。特定の値が更新された時だけ処理を行うことがuseEffectの最大のメリットです。例えば継続的にデータを取得し続けたなら、メモリリークが発生してしまうことだってあります。クラスコンポーネントの場合であれば、componentWillUnmountでクリーンアップ処理を行いますが、フックの場合はuseEffectで行います。
Json Placeholderからデータを取得して表示しよう
外部APIのデータを取得する場合はどうでしょうか。今回はJson Placeholderからデータを取得してレンダリング制御をやってみましょう。
上記のようなAPIの取得方法で考えてみます。行うことは
- selectboxを用意してonChangeイベントを設定する。その時セッター関数にvalueを渡す
- selectboxで選択された値をidとし、axiosで選択されたidに基づくデータを一件取得する
- 取得したデータを子コンポーネントにpropsで渡し、描画する
axoisの導入
npm i axios --save
axiosをインポートしてjson Placeholderのデータを取得しましょう。
ますは全コードです。
import React, {useState, useEffect} from 'react';
import axios from 'axios'
import './App.css';
import PostLists from './PostLists'
const App = () => {
const [ post, setPosts] = useState([])
const [ query, setQuery ] = useState('')
const [ loading, setLoading] = useState(false)
const [ error, setError] = useState(false)
useEffect(() => {
setLoading(true)
setError(false)
axios.get(`https://jsonplaceholder.typicode.com/posts/${query}`)
.then((res) => {
console.log(res)
const postData = res.data
setPosts(postData)
setLoading(false)
})
.catch(() => {
setError(true)
})
.finally(() => {
setLoading(false)
})
},[query])
return (
<div className="App">
<div className="render-container">
<select
onChange={(e) => setQuery(e.target.value)}>
<option>選択してください</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
<p>Id:{query}</p>
</div>
<div>
{
error ? (
<p
style={{color: "red"}}
>
データを取得できませんでした。
</p>)
:loading ? (
<p style={{color: "green"}}>loading...</p>
):(
<>
<PostLists key={post.id} post={post}/>
</>
)}
</div>
</div>
);
}
export default App;
// postLists.js
import React from 'react'
import './App.css'
const PostLists = (props) => {
const {post} = props
return (
<div className="todo">
<p>{post.id}</p>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
)
}
export default PostLists
上記のコードを少しずつみていきましょう。
まず必要なJSXやコンポーネントを作成します。作成するのはPostLists.jsです。PostLists.jsにはpropsを渡し、データを展開します。
次にApp.jsにPostList.jsをインポートしてJSXに登録します。
useEffectでデータを取得する
axiosとuseEffectを使ってデータを取得し、queryが変わるたびにstateを更新するようにします。
import React, {useEffect} from 'react'
useEffect(() => {
axios.get(`https://jsonplaceholder.typicode.com/posts/${query}`)
.then((res) => {
console.log(res)
const postData = res.data
setPosts(postData)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
console.log('close')
})
},[query])
データを取得した後、レスポンスをsetPostsセッター関数に代入しています。ここはstateの更新となります。また第二引数にquery配列が渡されています。queryの値が変わった時(レンダリングされた後)useEffectが呼ばれることになります。ではstateを定義します。
const [ post, setPosts] = useState([])
const [ query, setQuery ] = useState('')
getメソッドで取得したデータをpostData変数に格納してsetPostsセッター関数に代入します。セレクトボックスの値、すなわちqueryが更新されるたびにレンダリングが発生し、useEffectが発火します。もし第二引数のqueryが空配列であれば、初回の一回のみの取得となります。useState(1)のような特定のデータのみの実装であればそのような挙動になります。
useEffect内でstateの変更がある場合はそのstateが第二引数に渡されていることが自然な結果だと思います。queryが変わったら再取得するという一連の理屈が通ったフローです。データが変更されたらそれに伴ってシンクロしているのです。
JSX内のセレクトボックス
<select
onChange={(e) => setQuery(e.target.value)}>
<option>選択してください</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
valueの値がsetQueryに代入され、queryが更新され、レンダリングが走り、useEffectが発火します。
LodingとErrorを作成しよう
データの取得フェーズはloadingを表示させ、取得が完了したらloadingも伴って完了に変更したいですね。errorに関しても取得できなかったときはerrorを表示させましょう。
const [ loading, setLoading] = useState(false)
const [ error, setError] = useState(false)
共に初期値はfalseです。データフローの中で値を更新していきましょう。
useEffect(() => {
setLoading(true)
setError(false)
axios.get(`https://jsonplaceholder.typicode.com/posts/${query}`)
.then((res) => {
console.log(res)
const postData = res.data
setPosts(postData)
setLoading(false)
})
.catch(() => {
setError(true)
})
.finally(() => {
setLoading(false)
})
},[query])
初期段階でデータが取得できていない間はLoding表示したいのでtrueに更新し、errorは取れなかった時表示したいのでcatch内に設定します。取れても取れなくてもfinallyで実行されるのはloadingです。結果が出たのならloadingはオフにしましょう。
エラーとloadingのメッセージを表示しよう
- データが取得されなかった場合はエラーを表示させたい。
- データを取得している間はloadingを表示させたい。
- データを取得し終えたら、データを表示させたい。
このような場合はJSX内で三項演算子を用います。
<div>
{
error ? (
<p
style={{color: "red"}}
>
データを取得できませんでした。
</p>)
:loading ? (
<p style={{color: "green"}}>loading...</p>
):(
<>
<PostLists key={post.id} post={post}/>
</>
)}
</div>
ではpropsでデータを渡されたPostLists.jsはこちらです。
// postLists.js
import React from 'react'
import './App.css'
const PostLists = (props) => {
const {post} = props
return (
<div className="todo">
<p>{post.id}</p>
<p>{post.title}</p>
<p>{post.body}</p>
</div>
)
}
export default PostLists
まとめ
ここまででいかがだったでしょうか。useEffectとはなんなのか解説してみました。Reactはレンダリングの扱いがある意味課題だったりします。最適なアプリの状態とはどんな状態なのか、レンダリングがメモリリークの大きな負荷とならないように最適にするために適切な使い方が求められます。今回は基礎が多めの解説でしたがここまでありがとうございました。以上です。