先日とあるコードのリファクタリングを実施した際にちょっと気になるuseEffectの記述があったので今回記事を書いています。
おそらく「なんとなくReactをやっている」場合に起きがちな実装だと思うので、是非こちら参考にしていただければと思います。
また、誤記や解釈誤りなどあれば是非コメントをいただければと思います。
useEffectって何?
副作用を有する可能性のある命令型のコードを受け付けます。
とのことです。
全くなんのことか分かりませんね。
DOM の書き換え、データの購読、タイマー、ロギング、あるいはその他の副作用を、関数コンポーネントの本体(React のレンダーフェーズ)で書くことはできません。
要はこんなことはできないよということですね。
import React from 'react'
// HogeListを取得してきてくれる処理
import { fetchHogeList } from 'api'
// Promiseなコンポーネントとは・・・一体・・・
const Hoge = async () => {
// これをやりたいがためにとりあえずasyncをつけて実装してみた
// しかし書いている通り関数コンポーネントの本体でこういったことは書くことはできません
const list = await fetchHogeList()
const children = list.map(item => <li key={item.id}>{item.name}</li>)
return (
<ul>
{children}
</ul>
)
}
そしてこういうことを実現させるためにuseEffect
が存在します。
useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。
レンダーの結果が画面に反映された後に動作するというのがどういうことかというとDOMの書き換えが一番分かりやすいかと思います。
まだ要素が表示されていない、表示する準備ができていないというのは対象が存在していないので、書き換えをしたくてもできません。
逆にレンダーの結果が画面に反映されたあとというのは対象の要素が存在しているということなのでDOMの書き換えが可能になります。
そういった処理をしたい場合は、useEffectを使いましょうね。ということとなります。
useEffectを使ってみる
では早速useEffectを使ってみます。
その前に書き方を覚えましょう。
// 第一引数には関数を渡す
useEffect(() => {
// 実行させたい処理記述する
return () => {
// returnも記述可能です
// クリーンアップとして実行したい処理を記述する(省略可能)
}
// 第二引数には条件付きでuseEffectを実行したい際に依存したデータを記述する
// 空配列の場合は初回のみ実行されます
},[])
以上です。
では先ほどの処理を置き換えて書いてみましょう。
import React, { useState } from 'react'
import { fetchHogeList } from 'api'
const Hoge = () => {
// 処理結果を反映するstateを用意する、初期値は空配列
const [hogeList, setHogeList] = useState([])
// useEffectでfetch処理を行う
useEffect(() => {
// 非同期処理の場合は、関数を定義しそれを呼び出すような形式で記述すること
const fetch = async () => {
const newHogeList = await fetchHogeList()
setHogeList(newHogeList)
}
fetch()
}, [])
const children = list.map(item => <li key={item.id}>{item.name}</li>)
return (
<ul>
{children}
</ul>
)
}
どうでしょうか?
useEffectでデータを取得し、その結果をstateとして保持するような形式です。
今回で言うと初回のみ実行されれば良いので第二引数の依存配列には何も指定していません。
アクションによるデータの再取得を実装してみる
では、今回初回時にデータを取得していましたが以下の要件が加わりました。
- fetchHogeListはpage(数値)を引数として渡すことができる。渡さない場合は先頭の1ページ目のデータを取得している。
- データは20件ずつ取得しており、pageとして渡した部分のデータを取得している
- 当該のページにページネーションの機能を追加したい
- ボタンを押したら次のページのデータを閲覧できる(前のページは今のところ考えないこととする)
さて、まずはこちら要件通りに実装してみましょう。
今回追加する内容は「ボタンを押したら次の20件のデータが取得される」と言う機能です。
なお今回はuseCallbackなどは省略していますのがご理解ください。
import React, { useState } from 'react'
import { fetchHogeList } from 'api'
const Hoge = () => {
const [hogeList, setHogeList] = useState([])
// ページ番号を持っておく
const [currentPage, setCurrentPage] = useState(1)
useEffect(() => {
const fetch = async () => {
const newHogeList = await fetchHogeList(1)
setHogeList(newHogeList)
}
fetch()
}, [])
// クリック時の処理を実装
const onClick = async () => {
const nextPage = currentPage + 1
// 新たなhogeListを取得してくる
const newHogeList = await fetchHogeList(nextPage)
// stateをそれぞれ更新
setCurrentPage(nextPage)
setHogeList(newHogeList)
}
const children = list.map(item => <li key={item.id}>{item.name}</li>)
return (
<div>
<ul>
{children}
</ul>
<button onClick={onClick}>次へ</button>
</div>
)
}
さて実装してみました。
useEffectでは初回時のデータを取得しています。
そして新たに実装したonClickでは次のページのデータを取得しstateに反映しています。
これによりデータの更新が実現できました。
これで完成です。
リファクタリング
でもちょっと待ってください。
少しリファクタリングできそうではないですか?
なんだかuseEffectとonClickで同じようなことをしています。
これをリファクタリングして見たらもう少し綺麗なコードになること間違いなしです。
import React, { useState } from 'react'
import { fetchHogeList } from 'api'
const Hoge = () => {
const [hogeList, setHogeList] = useState([])
const [currentPage, setCurrentPage] = useState(1)
// fetch処理を別で切り出す
const fetch = async (page) => {
const newHogeList = await fetchHogeList(page)
setHogeList(newHogeList)
}
useEffect(() => {
// 内部のコードを別に切り出したので呼び出しを行うのみ
fetch()
}, [])
// クリック時の処理を実装
const onClick = async () => {
const nextPage = currentPage + 1
// フェッチ処理を共通化してそちらを呼び出す
await fetch(nextPage)
// ページの更新だけはこちらで実装
setCurrentPage(nextPage)
}
const children = list.map(item => <li key={item.id}>{item.name}</li>)
return (
<div>
<ul>
{children}
</ul>
<button onClick={onClick}>次へ</button>
</div>
)
}
できました。
リファクタリングでを行ったことでコードが共通化されました。
fetchの実装が多少変わってもこれなら1箇所の修正のみで対応可能になります。
コードも綺麗になっていいことづくめです。
さあ、これでReactで行う非同期通信を理解できましたね。
お疲れ様でした。
依存配列
と、まあここで終わるわけがないのです。
むしろここからが本題です。
ちょっとJavaScriptを触ったことがある人なら調べながら実装してみて、動作確認して動くのでこれでOKとなってしまう可能性があります。
冒頭で話したケースもこのようなパターンでした。
しかし思い出してみましょう。
useEffectには条件付きで副作用を実行する
という便利な機能があります。
つまりこれは「特定の値が更新された場合に副作用を実行する」ということです。
なので先ほどのコードはリファクタリングをして良しではなく、本来以下のように記述することができます。
React本来の機能を使って書くならむしろこちらが正しい考え方です。
import React, { useState } from 'react'
import { fetchHogeList } from 'api'
const Hoge = () => {
const [hogeList, setHogeList] = useState([])
const [currentPage, setCurrentPage] = useState(1)
useEffect(() => {
const fetch = async () => {
// page数をcurrentPageから引くように修正
const newHogeList = await fetchHogeList(currentPage)
setHogeList(newHogeList)
}
fetch()
// 依存配列にcurrentPageを追加
// currentPageの変更が起きた場合にuseEffectのfetchが実行される
}, [currentPage])
// クリック時の処理を実装
const onClick = () => {
// ページ数の更新のみを行う
setCurrentPage(currentPage + 1)
// fetch処理はonClick内では実施しない
// useEffectでcurrentPageが依存関係にあるので、変更されるとfetchが実行される、
// つまりここでfetchしてしまうと、むしろ2回のリクエストが必ず飛んでしまいパフォーマンス低下となってしまう
}
const children = list.map(item => <li key={item.id}>{item.name}</li>)
return (
<div>
<ul>
{children}
</ul>
<button onClick={onClick}>次へ</button>
</div>
)
}
どうですか?
かなりすっきりしたと思いませんか?
useEffectでは「初回のレンダー完了時」と「currentPage」に変更が入った場合にfetch処理が実行されます。
この機能を利用し、onClickではページの更新のみを行っています。
fetch自体はuseEffectで実施されるので検索条件の変更が検知された場合のみ、fetchするという仕組みができあがりました。
例えば今後他の条件が増えた場合にもそれぞれの値が依存するのはuseEffectのみになるので、アクション間で依存関係がなくなります。
あくまでその対象の条件を変更するという部分に影響範囲を閉じることができるようになります。
これがどう言う状況で便利かと言うと、例えばuseCallbackを使うケースなどで「値の変更はしていないけどfecthの条件変更は検知する必要があるので依存配列に渡さざるおえない」といったケースがなくなりパフォーマンスも可読性も向上します。
まとめ
useEffectというより特に依存配列にどんな値を渡せばいいか
を考えるのは最初のつまづきポイントかもしれません。
しかしここをうまく使えるとかなりすっきりしたコードが書けるようになるので是非この辺りマスターしていただければと思います!(という僕自身も修行中の身ですので今後とも勉強していきたいと思います)