関数コンポーネントで色々できるようになるReact Hooksです。
Reactいじったことある人前提で書いていきます。
目次
useState
useeffect
uselayouteffect
usecontext
usereducer
usecallback
usememo
useref
useimperativehandle
useState
コンポーネント内での状態管理を実装できます。
コードで再現すると
const { useState } = require("react")
function UseStateSample() {
const [count, setCount] = useState(0)
return(
<div>
<p>set count: {count}</p>
<div>
<button onClick={() => setCount(count - 1)}>-</button><button onClick={() => setCount(count + 1)}>+</button>
</div>
</div>
)
}
export default UseStateSample
実際にボタンが押されると、ボタンに応じてカウントが変更されて数字も変わっています。
useState
の使い方は
const [state, setState] = useState(initialState)
のように2つの要素を渡します。1つ目の要素には初めは初期値としてinitialState
で設定したものが入ります。2つ目の要素は1つ目の要素を更新するために使います。useState
のinitialState
にはstate
の初期値を設定しています。
今回の例だとcount
という状態変数を設定し、setCount
を使ってcount
を変更して、useState
にcount
の初期値を0で設定しています。
state
、setState
は任意の名前で決められます。setState
の方は特になければ寛容的にset + stateでつけた名前の先頭一文字目を大文字
で付けています。Reactを触ってる人はなんとなくわかると思います。
React18以降と以前では若干動きが異なると思います。自動バッチングやflushSyncとか
自動バッチングやflushSyncのわかりやすかった記事:
useEffect
副作用を実装できます。
ここでいう副作用は、関数の外に影響を与えるものと関数の引数以外で戻り値に影響を与えるものとなっています。例えば、DOMの書き換え、データ読み込み、タイマー、ロギングなどがあり、これらはuseEffect
を使用して扱うことができます。
クラスコンポーネントを使う人からするとuseEffect
はcomponentDidMount
、componentDidUpdate
、componentWillUnmount
をまとめたものとなっています。
データ読み込みの例を使って説明すると:
import { useEffect, useState } from "react"
function UseEffectSample() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://www.reddit.com/r/reactjs.json')
.then(response => response.json())
.then(json => setData(json.data.children.map(content => content.data)))
})
return(
<div>
{data.map(d => (
<li key={d.id}>{d.title}</li>
))}
</div>
)
}
export default UseEffectSample
と表示されてしっかり外部からデータを取得することができます。
実はこのコードだと少し問題があります。これだとレンダーの完了時に毎回呼び出されてしまいます。それをさせないためにuseEffect
には配列で受け取れる第2引数があります。
先ほどのコードを少し作り替えていきます。今回は二つのコンポーネントを使っていきます。
import { useEffect, useState } from "react"
function Reddit({subreddit}) {
const [data, setData] = useState([])
useEffect(() => {
fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => setData(json.data.children.map(content => content.data)))
}, [subreddit])
return(
<div>
{data.map(d => (<li key={d.id}>{d.title}</li>))}
</div>
)
}
export default Reddit
import { useState } from "react"
import Reddit from "./Reddit"
function UseEffectSample2() {
const [inputValue, setInputValue] = useState('')
const [submitValue, setSubmitValue] = useState('reactjs')
const handleSubreddit = (event) => {
setInputValue(event.target.value)
}
const handleSubmit = (event) => {
event.preventDefault()
setSubmitValue(inputValue)
}
return(
<div>
<form onSubmit={handleSubmit}>
<label>
input subreddit:
<input type='text' name='subreddit' onChange={handleSubreddit}></input>
</label>
<input type='submit' value='submit'></input>
</form>
<Reddit subreddit={submitValue}></Reddit>
</div>
)
}
export default UseEffectSample2
これを実行すると
ここで何をしているかの説明は、フォームで入力したテキストをRedditのsubredditのAPIに使って、jsonがあれば画面を変更して表示するようにしています。
ここで重要なのがuseEffect
の第2引数にsubreddit
が入っています。これはsubreddit
が変更されると次のレンダー時にsubreddit
が変更されていればuseEffect
が走ります。
useEffect
の第2引数では配列で設定することができ、useEffect
を実行するタイミングを調整することができます。初回レンダーのみで呼び出したい時は第2引数を[]
にし、値の変更に応じてuseEffect
を実行したいときは監視したい値を第2引数に配列で設定することでできます。
クラスコンポーネントではcomponentWillUnmount
でクリーンアップを行っていたことをuseEffect
でも行うことができます。クリーンアップを先ほどのReddit.js
の例で少し書き加えると
import { useEffect, useState } from "react"
function Reddit({subreddit}) {
const [data, setData] = useState([])
const controller = new AbortController()
const signal = controller.signal
useEffect(() => {
fetch(`https://www.reddit.com/r/${subreddit}.json`, { signal })
.then(response => response.json())
.then(json => setData(json.data.children.map(content => content.data)))
// クリーンアップ
return () => {
controller.abort()
}
}, [subreddit, data])
return(
<div>
{data.map(d => (<li key={d.id}>{d.title}</li>))}
</div>
)
}
export default Reddit
のようにクリーンアップはuseEffect
のreturn
を追加してその中にクリーンアップ処理を追加できます。
ところで最初の方でuseEffect
はcomponentDidMount
、componentDidUpdate
、componentWillUnmount
をまとめたものと言いましたが、どの辺がまとまったものなのかというと:
-
componentDidMount
はuseEffect
の第2引数を設定した状態 -
componentDidUpdate
はuseEffect
自体がstate
を監視して、state
に更新があれば実行されるところ -
componentWillUnmount
はuseEffect
のクリーンアップの部分
になっていると思います。
useEffect
で参考になったリンクたち:
↓useEffect
の第2引数を省略しちゃいけない理由
↓何で参考にしたのか忘れました
↓useEffect
がcomponentDidUpdate
の機能を備えた理由
↓useEffect
の第2引数を設定することによるパフォーマンス向上
useLayoutEffect
useEffect
のおまけとして書いていきます。
これの機能はuseEffect
とほとんど同じです。使い方はuseEffect
と変わらず、第1引数と第2引数があり、useEffect
の使い方と意味は同じです。
唯一の違いは呼び出されるタイミングが画面が描画される前か後かです。useLayoutEffect
は描画される前に呼び出されます。実際のところこれを使う場面は稀だと思います。下手に使おうとするとパフォーマンスに影響が出たりするので、初めに絶対に表示させたいものだけを実装することをお勧めします。
useLayoutEffect
の参考サイト
↓useLayoutEffect
のドキュメント
useContext
クラスコンポーネントにもあったコンテキストのフック版です。
クラスコンポーネントと比べるとちょっと実装が楽になった感じです。コンポーネントを3つ使って説明します。
import { createContext } from "react"
import ComponentA from "./ComponentA"
export const ContextToPassToComponentB = createContext()
function UseContextSample() {
return(
<div>
<ContextToPassToComponentB.Provider value='ComponentBに渡したよ'>
<p>ここが1番上のコンポーネント</p>
<ComponentA anyProps='親から受け取った'></ComponentA>
</ContextToPassToComponentB.Provider>
</div>
)
}
export default UseContextSample
このコンポーネントが1番上のコンポーネントとして使います。
次がUseContextSample.js
の子となるComponentA.js
です。
import ComponentB from "./ComponentB"
function ComponentA({anyProps}) {
return(
<div>
<p>ComponentA</p>
<p>{anyProps}</p>
<ComponentB></ComponentB>
</div>
)
}
export default ComponentA
次がComponentA.js
の子となるComponentB.js
です。
import { useContext } from "react"
import { ContextToPassToComponentB } from "./UseContextSample"
function ComponentB() {
const context = useContext(ContextToPassToComponentB)
return(
<div>
<p>ComponentB</p>
<p>{context}</p>
<p></p>
</div>
)
}
export default ComponentB
コンテキストの説明は不要だと思いますが、コンテキストを使わないとUseContextSample.js
からComponentB.js
へと値を渡すにはprops
を一旦ComponentA.js
を通してから渡すかUseContextSample.js
に直接ComponentB
コンポーネントを実装するかになると思います。階層が少なければなんとかなると思いますが、100階層以上とか1つのコンポーネントに100個以上(どこかのプロジェクトであるかもしれぬ...)になった場合はprops
の追跡が大変になると思います。なるべくそうならないためにコンテキストが有効になると思います。
実際にこのコードを実行すると
となってUseContextSample.js
からComponentB.js
に直接値を渡せています。今回は普通の値を渡していますがstate
も渡せたりできます。
↓useContext
の公式ドキュメント
useReducer
useReducer
はuseState
の拡張版みたいなものです。
定義は
const [state, dispatch] = useReducer(reducer, initialArg, init);
となっています。これは公式的にはReduxをやってると馴染みがあるらしいのですが私はやってないのでよくわからないです。私自身使ったことないので触った感じで書いていきます。
普段使っている分にはstate
の管理はsetState
で十分ですが、複数の値にまたがる複雑なstate
ロジックがある場合や、前のstate
に基づいて次のstate
を決める必要がある場合(公式から)やuseContext
で少し触れたようにstate
の受け渡しが関わる複数階層コンポーネントへの受け渡しの時に有効です(パフォーマンスの最適化)。
useReducer
のよくある例だと第3引数まで使われているものが少ないので、まずは第1と第2が使われているものを使っていきます。次のコードは2で足したり割ったりする四則演算を例に書いていきます。
import { useReducer } from "react"
function UseReducerSample() {
const initResult = 0.0
const [result, calculation] = useReducer((currentSResult, action) => {
let type = action.type
switch(type) {
case 'plus':
return currentSResult + 2
case 'minus':
return currentSResult - 2
case 'multiply':
return currentSResult * 2
case 'divide':
return currentSResult / 2
case 'reset':
return initResult
}
}, initResult)
return(
<div>
<p>この値から計算します:{result}</p>
<p><button onClick={() => calculation({type: 'plus'})}>2足す</button></p>
<p><button onClick={() => calculation({type: 'minus'})}>2減らす</button></p>
<p><button onClick={() => calculation({type: 'multiply'})}>2でかける</button></p>
<p><button onClick={() => calculation({type: 'divide'})}>2でわる</button></p>
<p><button onClick={() => calculation({type: 'reset'})}>リセット</button></p>
</div>
)
}
export default UseReducerSample
これを実行すると
のように2で四則演算を実装できました。
このコードでのuseReducer
の説明をすると、[result, calculation]
の部分はresult
はuseState
の部分で言うstate
になり、culculation
はresult
の結果を出す関数になってます。そしてuseReducer
の第1引数はculculation
が呼び出されたときに実際に実行する処理を実装する部分になります。この中で関数に渡された値から計算してresult
に出していくことをしています。第2引数はresult
の初期値の設定部分です。
もう少し説明していくと、第1引数で実装した関数内で(currentSResult, action)
がありますが、currentSResult
は名前の通り現在のresult
、すなわち今のstate
を表しています。action
はculculation
で受け取った引数が入ります。
ついでにこれをsetState
を使ったバージョンで書くと
import { useState } from "react"
function UseStateSample2() {
const initResult = 0.0
const [result, setResult] = useState(initResult)
const calculation = (action) => {
let type = action.type
switch(type) {
case 'plus':
setResult(result + 2)
break
case 'minus':
setResult(result - 2)
break
case 'multiply':
setResult(result * 2)
break
case 'divide':
setResult(result / 2)
break
case 'reset':
setResult(initResult)
break
}
}
return(
<div>
<p>この値から計算します:{result}</p>
<p><button onClick={() => calculation({type: 'plus'})}>2足す</button></p>
<p><button onClick={() => calculation({type: 'minus'})}>2減らす</button></p>
<p><button onClick={() => calculation({type: 'multiply'})}>2でかける</button></p>
<p><button onClick={() => calculation({type: 'divide'})}>2でわる</button></p>
<p><button onClick={() => calculation({type: 'reset'})}>リセット</button></p>
</div>
)
}
export default UseStateSample2
で表現できます。
最初のところでuseReducer
には第3引数があることを書きました。これの正体はstate
の初期化を関数で実装できるものになってます。さっきのコードを少し書き換えると
import { useReducer } from "react"
function UseReducerSample() {
const initResult = 0.0
// これ追加(初期値に100を追加)
const init = (initValue) => {
return initValue + 100
}
const [result, calculation] = useReducer((currentSResult, action) => {
let type = action.type
switch(type) {
case 'plus':
return currentSResult + 2.0
case 'minus':
return currentSResult - 2.0
case 'multiply':
return currentSResult * 2.0
case 'divide':
return currentSResult / 2.0
case 'reset':
return initResult
}
}, initResult, init) // 第3引数にinit追加
return(
<div>
<p>この値から計算します:{result}</p>
<p><button onClick={() => calculation({type: 'plus'})}>2足す</button></p>
<p><button onClick={() => calculation({type: 'minus'})}>2減らす</button></p>
<p><button onClick={() => calculation({type: 'multiply'})}>2でかける</button></p>
<p><button onClick={() => calculation({type: 'divide'})}>2でわる</button></p>
<p><button onClick={() => calculation({type: 'reset'})}>リセット</button></p>
</div>
)
}
export default UseReducerSample
これを実行すると
ほとんど変わらないのですが、初期値が0なのが嫌だったのか元々ある初期値に100を追加するように設定しています。init
で受け取った引数から100を追加することで初期値に100が追加された状態の初期値が初めに表示されています。useReducer
ではこの関数を第3引数に入れて、第2引数をinit
の引数として受け取っています。
これを使うことでReduxのようなことができるのですが公式的にはおすすめされていません。
参考にしたサイト:
↓useReducer
の公式ドキュメント
↓useReducer
を使う利点1
useCallback
メモ化されたコールバックを実装できます。
メモ化自体はパフォーマンスの最適化として使われますが、Reactでは少し意味が異なり、useCallback
ではコンポーネント間にある依存関係から不要なレンダーを防ぐときに有効になってきます(もちろん不要なレンダーが減るのでパフォーマンスが良くなるという意味にもなります)。
比較のため、useCallback
を使わなかった例と使う例を書いていきます。
useCallback
を使わなかった例は
import { useState } from "react"
import ComponentC from "./ComponentC"
function ComponentSample() {
console.log('親コンポーネントレンダー')
const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)
// 子コンポーネントでボタンがクリックされた時に実行
const handleClick = () => {
setCount2(() => count2 + 1)
}
// 親コンポーネントでボタンをクリックすると実行
const plusCount = () => {
setCount(() => count + 1)
}
return(
<div>
<p>私が親です:{count}</p>
<p>私は子です:{count2}</p>
<button onClick={plusCount}>カウントを増やす</button>
<ComponentC handleClick={handleClick}></ComponentC>
</div>
)
}
export default ComponentSample
function ComponentD(props) {
console.log('子コンポーネントレンダー')
return(
<div>
<p><button onClick={props.handleClick}>ComponentDでカウントを増やす</button></p>
</div>
)
}
export default ComponentD
これを実行すると
のように親と子に実装されているボタンをクリックするたびに親子のコンポーネント両方ともレンダーされています。
この時、親コンポーネントでのみの処理をしている時にもかかわらず、子コンポーネントが呼び出されています。これが不要なレンダーと考えられます。これをなくすためにuseCallback
があります。
useCallback
を使った例は
import { useCallback, useEffect, useState } from "react"
import ComponentC from "./ComponentC"
function UseCallbackSample() {
console.log('親コンポーネントレンダー')
const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)
// 子コンポーネントでボタンがクリックされた時に実行
const handleClick = useCallback(() => {
setCount2(() => count2 + 1)
}, [count2])
// 親コンポーネントでボタンをクリックすると実行
const plusCount = () => {
setCount(() => count + 1)
}
return(
<div>
<p>私が親です:{count}</p>
<p>私は子です:{count2}</p>
<button onClick={plusCount}>カウントを増やす</button>
<ComponentC handleClick={handleClick}></ComponentC>
</div>
)
}
export default UseCallbackSample
import { memo } from "react"
function ComponentC(props) {
console.log('子コンポーネントレンダー')
return(
<div>
<p><button onClick={props.handleClick}>ComponentCでカウントを増やす</button></p>
</div>
)
}
export default memo(ComponentC)
これを実行すると、
のように子コンポーネントが呼ばれず不要なレンダーが避けられるようになっています。
useCallback
を使うときの注意点として、useCallback
を使う先のコンポーネントにmemo
の中にコンポーネントを入れないと機能しないことに注意してください。useCallback
の第2引数にuseEffect
のような配列がありますが、これは依存配列になっていて、useeffect
と同じで、配列内の値が変更されると実行されます。
ただ、useCallback
自体、普通のちょっとした関数と比べると多少パフォーマンスが高いので不要な仕様は控えるようにしたほうがいいです。本当に必要な時のみ使用するようにしてください。
参考にしたサイト:
↓useCallback
の公式ドキュメント
useMemo
メモ化された値を実装できます。
useCallback
と使う場面がほとんど同じなので特に記載はないです。
参考にしたサイト:
useMemo
公式ドキュメント
useRef
ミュータブルなref
オブジェクトを実装できます。これはミュータブルな値を.current
に保存します。実際に値を使用するときはref(変数名はなんでも良い).current
で使用します。
Reactをよく使っている人はDOMにアクセスできるやつでいつもお世話になっているものですね。
使う用途としては先ほどのDOMへのアクセスに使われることとミュータブルなものとして値を保存するものとして使えます。
まずミュータブルな値を保存するものとして使う例は
import { useRef, useState } from "react"
function UseRefSample() {
let normalValue = '普通の値です'
const [state, setState] = useState('stateの値です')
const refValue = useRef('refの値です')
// 値を変更しているがレンダーされたらなくなる
const normalValueChange = (event) => {
normalValue = event.target.value
}
const stateChange = (event) => {
setState(event.target.value)
}
const refValueChange = (event) => {
refValue.current = event.target.value
}
return(
<div>
<p>{normalValue}</p>
<p>{state}</p>
<p>{refValue.current}</p>
<div>
<p>普通の値:<input type='text' onChange={normalValueChange}></input></p>
</div>
<div>
<p>stateの値:<input type='text' onChange={stateChange}></input></p>
</div>
<div>
<p>refの値:<input type='text' onChange={refValueChange}></input></p>
</div>
</div>
)
}
export default UseRefSample
これを実行すると
というようになり、ref
の値を変更してからstate
の値を変更するとref
の方の値も更新されています。ここで気づいた人もいるかもしれませんが、メモ化をしています。useRef
はstate
のように値を変更しても再レンダリングされません。state
が更新されて再レンダリングされてもref
の値は保持されます。
そして、DOMにアクセスする例(こっちの方がいつも使われていると思います)は
import { useRef } from "react"
function UseRefSample2() {
const focusRef = useRef(null)
const handleClick = () => {
focusRef.current.focus()
}
return(
<div>
<p>フォーカスする対象:<input type='text' ref={focusRef}></input></p>
<button onClick={handleClick}>フォーカス!!</button>
</div>
)
}
export default UseRefSample2
これを実行すると
のようになります。.current
にDOMの情報が保持されそこからDOMの操作を行うことができます。
参考にしたサイト:
↓useRef
のドキュメント
インスタンス変数について
オブジェクト作成の遅延方法
useImperativeHandle
子コンポーネントのメソッドをref
経由で親コンポーネントで使うことができます。
公式ドキュメントでもあるようにあまりこのフックを使うこと自体はおすすめされていません。おそらく使う場面はないとは思いますが一応書いていきます。
useImperativeHandle
の使い方は
useImperativeHandle(ref, createHandle, [deps])
となっていて、第1引数にはref
を入れて、第2引数にはそれぞれメソッドを定義していく場所で、第3引数はuseEffect
のように依存配列を入れるところです。ここでは第2引数までの例で説明していきます。
import { useRef } from "react"
import ComponentF from "./ComponentF"
function UseImperativeHandleSample() {
const parentRef = useRef(null)
const handlefocus = () => {
parentRef.current.focus()
}
return(
<div>
<ComponentF ref={parentRef}></ComponentF>
<button onClick={handlefocus}>フォーカス!!</button>
</div>
)
}
export default UseImperativeHandleSample
import { forwardRef, useImperativeHandle, useRef } from "react";
function ComponentF({props}, ref) {
const inputRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}))
return(
<div>
<input ref={inputRef}></input>
</div>
)
}
export default forwardRef(ComponentF)
これを実行すると、
となります。reactのref
の例がフォーカスばかりなのでフォーカス以外もやりたかったのですがなんも思いつかなかったので結局フォーカスにしました。
参考にしたサイト:
↓useImperativeHandle
の公式ドキュメント
forwardRef
で参考にしたサイト
↓forwardRef
公式ドキュメント
終わり
これくらいで終わろうと思います。他にもuseDeferredValue
、useTransition
、useId
もありますが機会があれば書いていきたいなと思います。