React Hooks その2
前回の記事で基本的なReact Hooksについて復習しました。
今回はその続きとなります。
useReducer
useReducer
はuseState
と同じく状態管理を行うためのHookです。
以下のような構文になっております。
const [state, dispatch] = useReducer(reducer, initialArg, init);
state
が管理したい状態、dispatch
がreducer
を呼び出すための関数です。実際の状態変更はreducer
が行います。
state
の初期値はinitialArg
となります。
reducer
は以下のような関数です。
const reducer = (state, action) => newState;
state
とaction
を引数に取り、新しい状態newState
を返すような関数です。
実際に書いてみるとこんな感じです。
import React, { useReducer } from "react"; export const Sample = () => { const defaultValue = { hoge: 0, fuga: 0 } const reducer = (state, action) => { switch (action.type) { case 'hoge': return { ...state, hoge: state.hoge + 1 } case 'fuga': return { ...state, fuga: state.fuga + 1 } case 'hogefuga': return { hoge: state.hoge + 1, fuga: state.fuga + 1 } default: throw new Error() } } const [state, dispatch] = useReducer(reducer, defaultValue) return ( <div> <p>hoge: {state.hoge}</p> <p>fuga: {state.fuga}</p> <div> <button onClick={() => dispatch({ type: 'hoge' })}>hoge</button> <button onClick={() => dispatch({ type: 'fuga' })}>fuga</button> <button onClick={() => dispatch({ type: 'hogefuga' })}>hogefuga</button> </div> </div> ) }
reducer
の引数action
のtype
によって処理を切り替えているのがわかります。
これを実際に使ってみると、
hoge
ボタンを押したらhoge
の数値が、fuga
ボタンを押したらfuga
の数値が、hogefuga
ボタンを押したら両方の数値がインクリメントするコンポーネントができました。
最初に述べた通りuseReducer
はuseState
と同様に状態管理を行うためのHookですが、複雑な状態変更を伴う場合にはuseState
よりも適していると思います。
例えば今回のようにオブジェクト内の複数の値を更新したりする場合や、action.type
がより多い場合にも細かく条件設定することができます。
また、reducer
を切り分けることもできるので関数単体でのテストもできそうです。
ちなみに第3引数のinit
を使うことでstate
の初期値の設定を遅らせることができます。例えばprops
で渡ってきた値を初期値に設定したい場合等です。
import React, { useReducer } from "react"; export const Sample = ({ count }) => { const init = (count) => { return { count: count } } const reducer = (state, action) => { switch (action.type) { case 'increment': return { count: state.count + 1 } case 'decrement': return { count: state.count - 1 } case 'reset': return init(action.payload) default: throw new Error() } } const [state, dispatch] = useReducer(reducer, count, init) return ( <div> <p>count: {state.count}</p> <div> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>reset</button> </div> </div> ) }
こんな感じにして<Sample count={0} />
としてやると、
props
で渡ったcount
の値を元にstate
の初期値が設定され、またaction.type
がreset
の場合はaction.payload
の値で初期化されるコンポーネントになりました。
useMemo
useMemo
はパフォーマンス改善のためのHookです。関数の結果をメモ化しておき、それを返します。依存する変数の値が更新した場合のみ再度関数を実行します。
const memo = useMemo(() => { doSomething() }, [deps])
初回はdoSomething()
が実行されますが、2回目以降はdeps
の値が更新されない限り初回と同じ値を返します。
例として以下のようなコンポーネントを考えます。
import React, { useState } from "react"; export const Sample = () => { const [hoge, setHoge] = useState(0) const [fuga, setFuga] = useState(0) return ( <div> <div> <button onClick={() => setHoge(hoge + 1)}>hoge</button> <button onClick={() => setFuga(fuga + 1)}>fuga</button> </div> <Child count={hoge} /> </div> ) } export const Child = ({ count }) => { const superHeavyFunction = () => { // めっちゃ重い処理 console.log('hoge') return <p>{count} times clicked hoge.</p> } return ( superHeavyFunction() ) }
hoge
をクリックしたら子コンポーネントの文言が変わる、というものです。
これを動かしてみると、
fuga
をクリックしたときもコンソールにhoge
と出てしまう=superHeavyFunction()
が都度実行されてしまいます。
これは親コンポーネントでuseState
を使っているfuga
の値が更新されたため、子コンポーネントも再レンダリングされたからです。
けどその都度superHeavyFunction()
が実行されるとパフォーマンス的にかなり悪くなりそうです。
このような時にuseMemo
を使います。
import React, { useState, useMemo } from "react"; export const Sample = () => { const [hoge, setHoge] = useState(0) const [fuga, setFuga] = useState(0) return ( <div> <div> <button onClick={() => setHoge(hoge + 1)}>hoge</button> <button onClick={() => setFuga(fuga + 1)}>fuga</button> </div> <Child count={hoge} /> </div> ) } export const Child = ({ count }) => { const superHeavyFunction = useMemo(() => { // めっちゃ重い処理 console.log('hoge') return count }, [count]) return ( <p> { superHeavyFunction } times clicked hoge.</p> ) }
superHeavyFunction
をuseMemo
でラップすると、
hoge
を押した時だけコンソールにhoge
と表示され、fuga
を押した時には何も出なくなりました。
useRef
useRef
はcurrent
プロパティに引数で渡された値を持つオブジェクトを返します。
const ref = useRef(0); console.log(ref.current); // 0
よく使われるのはDOMへの参照です。公式にもその例が載っています。
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
ボタンをクリックするとinput
タグにフォーカスする、というものです。
しかし、useRef
には以下のような特徴があります。
useRef は中身が変更になってもそのことを通知しないということを覚えておいてください。.current プロパティを書き換えても再レンダーは発生しません。
これを利用して、再レンダリングの回数を減らすことができます。
import React, { useState, useRef } from "react"; export const Sample = () => { const [text, setText] = useState('') return ( <div> <input type="text" value={text} onChange={e => setText(e.target.value)} /> <p>{ text }</p> </div> ) }
上記のようなコンポーネントでは文字が入力される度に再レンダリングが走ります。
import React, { useState, useRef } from "react"; export const Sample = () => { const inputEl = useRef(null) const [text, setText] = useState('') const handleClick = () => { setText(inputEl.current.value) } return ( <div> <input ref={inputEl} type="text" /> <div> <button onClick={handleClick}>set</button> </div> <span>{ text }</span> </div> ) }
こうすることで文字を入力後にボタンを押した時のみ再レンダリングが行われるようになります。
まとめ
前回の続きとしてuseReducer
, useMemo
, useRef
についてまとめました。
Reactは便利ですが使い方を誤るとパフォーマンスに悪影響を及ぼす箇所も多いように思うのでこれらのHooksを使って最適化できるようにしたいと思います。