はじめに
2017年~2018年あたりでReact 16.x.xを使っていましたが、それ以降しばらく触っていませんでした。
去年の終わりくらいから改めてReactを触ろうとしたところ、Hooksなる機能がReact 16.8から追加されたということで、触ってみた際の学びを備忘録として残しておきます。Hooksいいですね!
対象Ver: 16.12.0
公式ドキュメント
https://ja.reactjs.org/docs/hooks-intro.html
※以降のコードは、私はこんな雰囲気で書いたんじゃよ、という備忘録ですので動作保証は致しません。
※私の検証ベースで記載している部分があるので、間違っていた場合はご指摘頂けると嬉しいです。
useState
state管理のHook。管理したいstate単位にuseStateを実行し、戻り値としてstate自身とそのsetter(setStateみたいなもの)を受け取る。useStateの引数は初期値。
import React, { useState } from 'react'
import LoadingIcon from '../icons/loading'
const App = props => {
const [content, setContent] = useState()
// 何かアクションに応じてデータを取得
const loadData = () => {
apiCall().then(result => {
setContent(result) // 取得したコンテンツを表示
})
}
return (
<div>
<button onClick={loadData}>Load</button>
<p>{content}</p>
</div>
)
}
らくちん。
useEffect
stateの変化を検知して処理を行う場合に使う。
import React, { useState, useEffect } from 'react'
const App = props => {
const [content, setContent] = useState()
const [filteredContent, setFilteredContent] = useState()
const loadData = () => {
// 省略
}
useEffect(() => {
// 何か処理をしてセット
const filteredContent = filter(content)
setFilteredContent(filteredContent)
}, [content])
return (
<div>
<button onClick={loadData}>Load</button>
<p>{filteredContent}</p>
</div>
)
}
第二引数の配列には、ウォッチしたいstateを指定する。今回であればcontentが変化した際に処理を実行したいので、contentを指定。
useRef
何かしらの参照を持っておくためのハコみたいなイメージ。(段々説明が雑になってきました)
公式ドキュメントにもありますが、Reactコンポーネントにref={}で渡して、コンポーネントの参照を持って置くためのものと思っていましたが、汎用的な箱として利用可能です。
具体例を示した方が分かりやすいので、実際に私がはまった例とその解決策を。
データロード中かどうかをstateで管理して、多重ロードを避けるために書いたコードが以下。
import React, { useState } from 'react'
const App = props => {
const [content, setContent] = useState()
const [loading, setLoading] = useState(false)
const loadData = () => {
// ロード中ならスキップ
if (loading) return
setLoading(true) // ロード中に設定
apiCall().then(result => {
setContent(result)
setLoading(false) // ロード中ステータスを解除
})
}
return (
<div>
<button onClick={loadData}>Load</button>
<p>{content}</p>
</div>
)
}
これだとうまくいきませんでした。
なぜか。loadData関数を定義した時点でClosureにその時点のloading変数の内容を保持されるので、いつまで経ってもloadingはfalseのままでした。
なのでこうしました。
import React, { useState, useRef } from 'react'
const App = props => {
const [content, setContent] = useState()
const loadingRef = useRef()
loadingRef.current = false // 初期化
const loadData = () => {
// ロード中ならスキップ
if (loadingRef.current) return
loadingRef.current = true // ロード中に設定
apiCall().then(result => {
setContent(result)
loadingRef.current = false // ロード中ステータスを解除
})
}
return (
<div>
<button onClick={loadData}>Load</button>
<p>{content}</p>
</div>
)
}
useRefの戻りはオブジェクトなので、それをClosureで持っておけば現在の値が参照可能なので、正しく動作するようになりました。
たぶん使い方は合っているハズ。。
2020/4/3 追記
stateが変わった時点で再Renderされる気がするので、上記結論は尚早かも。。
描画内容にloadingのstateが無かったから再Renderされなかった??要検証。
useReducer
最初に書きましたが、Reduxのaction/reducerなどの記述量の多さが苦手で、できれば避けたいと思っていましたが、避けられない場面が出てきました。
サーバから取得した結果を順々に配列に追加していくような、以下のコードを書いてみました。
import React, { useState, useRef } from 'react'
const App = props => {
const { seq } = props
const [results, setResults] = useState([])
// コールバック参照用
const resultsRef = useRef()
resultsRef.current = results
useEffect(() => { resultsRef.current = results }, [results])
const addResult = result => {
// 別オブジェクトにしないとReactが変更を検知しないので、別配列として処理
const newResults = resultsRef.current.concat(result)
setResults(newResults)
}
// 何かアクションに応じてデータを取得
const loadData = (sequence) => {
apiCall(sequence).then(result => {
if (result.status === 404) return
addResult(result) // 結果を配列に追加
loadData(sequence + 1) // 最新を取得するまでループ
})
}
return (
<div>
<button onClick={() => { loadData(seq) }}>Load</button>
<p>{content}</p>
</div>
)
}
これでうまくいくかと思いきや、追加したデータが消えていたりする。。。
今試してみたサンプルコードは以下。
const [arr, setArr] = useState([])
const arrRef = useRef()
arrRef.current = arr
useEffect(() => {
arrRef.current = arr
console.log('-----from-----')
console.log(arrRef.current.length)
console.log(arrRef.current)
console.log('-----to-----')
}, [arr])
useEffect(() => {
for(let i=0; i<100; i++) {
const newArr = arrRef.current.slice()
newArr.push(i)
setArr(newArr)
}
}, [])
結果はこう。
-----from-----
0
[]
-----to-----
-----from-----
1
[99]
-----to-----
前のstateを踏まえて何か処理する場合、useRefで参照を持っていても不十分だったようです。
そこでuseReducerの出番。
const sampleReducer = (state, action) => {
switch(action.type) {
case 'add':
const newArr = state.slice()
newArr.push(action.payload)
return newArr
default:
return state
}
}
const [arr, dispatch] = useReducer(sampleReducer, [])
useEffect(() => {
console.log('-----from-----')
console.log(arr.length)
console.log(arr)
console.log('-----to-----')
}, [arr])
useEffect(() => {
for(let i=0; i<100; i++) {
dispatch({ type: 'add', payload: i })
}
}, [])
結果はこう。
-----from-----
0
[]
-----to-----
-----from-----
100
(100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
-----to-----
100回出力されていないのは、おそらくdispatchを頻繁に実行したため、reducer側が良しなに更新タイミングを減らしてくださったのではと予想。
Reduxをご存知の方はお分かりと思いますが、前のstateを受けて処理が可能なので、addした分だけ情報が格納されています。
なので、前の状態に+αで変更する際はuseReducerを使うべき、というのが学びです。
そしてuseReducerを使ってみて思いましたが、結構簡素に書けますね。
以前はTypeScriptを使っていたこともあり、余計冗長に感じてしまったのかもしれません。
まとめ
- useStateはstateとそのsetterを返す
- stateがオブジェクトの場合、setterに指定するのは新しいオブジェクトにすること(Reactが検知できないっぽい)
- useEffectはstateの変更を検知して処理を行うヤツ
- useRefは使いやすいハコ
- useReducerは前のstateを踏まえて処理したい場合に有効