2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React Hooks

Last updated at Posted at 2023-02-08

関数コンポーネントで色々できるようになる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

これの結果が
qiita1.gif

実際にボタンが押されると、ボタンに応じてカウントが変更されて数字も変わっています。
useStateの使い方は

const [state, setState] = useState(initialState)

のように2つの要素を渡します。1つ目の要素には初めは初期値としてinitialStateで設定したものが入ります。2つ目の要素は1つ目の要素を更新するために使います。useStateinitialStateにはstateの初期値を設定しています。

今回の例だとcountという状態変数を設定し、setCountを使ってcountを変更して、useStatecountの初期値を0で設定しています。

statesetStateは任意の名前で決められます。setStateの方は特になければ寛容的にset + stateでつけた名前の先頭一文字目を大文字で付けています。Reactを触ってる人はなんとなくわかると思います。

React18以降と以前では若干動きが異なると思います。自動バッチングやflushSyncとか

自動バッチングやflushSyncのわかりやすかった記事:

useEffect

副作用を実装できます。

ここでいう副作用は、関数の外に影響を与えるもの関数の引数以外で戻り値に影響を与えるものとなっています。例えば、DOMの書き換え、データ読み込み、タイマー、ロギングなどがあり、これらはuseEffectを使用して扱うことができます。

クラスコンポーネントを使う人からするとuseEffectcomponentDidMountcomponentDidUpdatecomponentWillUnmountをまとめたものとなっています。

データ読み込みの例を使って説明すると:

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

これを実行すると
スクリーンショット 2023-01-14 15.01.15.png

と表示されてしっかり外部からデータを取得することができます。

実はこのコードだと少し問題があります。これだとレンダーの完了時に毎回呼び出されてしまいます。それをさせないためにuseEffectには配列で受け取れる第2引数があります。
先ほどのコードを少し作り替えていきます。今回は二つのコンポーネントを使っていきます。

React.js
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
UseEffectSample2.js
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

これを実行すると

qiita2.gif

ここで何をしているかの説明は、フォームで入力したテキストをRedditのsubredditのAPIに使って、jsonがあれば画面を変更して表示するようにしています。

ここで重要なのがuseEffectの第2引数にsubredditが入っています。これはsubredditが変更されると次のレンダー時にsubredditが変更されていればuseEffectが走ります。

useEffectの第2引数では配列で設定することができ、useEffectを実行するタイミングを調整することができます。初回レンダーのみで呼び出したい時は第2引数を[]にし、値の変更に応じてuseEffectを実行したいときは監視したい値を第2引数に配列で設定することでできます。

クラスコンポーネントではcomponentWillUnmountでクリーンアップを行っていたことをuseEffectでも行うことができます。クリーンアップを先ほどのReddit.jsの例で少し書き加えると

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

のようにクリーンアップはuseEffectreturnを追加してその中にクリーンアップ処理を追加できます。

ところで最初の方でuseEffectcomponentDidMountcomponentDidUpdatecomponentWillUnmountをまとめたものと言いましたが、どの辺がまとまったものなのかというと:

  • componentDidMountuseEffectの第2引数を設定した状態
  • componentDidUpdateuseEffect自体がstateを監視して、stateに更新があれば実行されるところ
  • componentWillUnmountuseEffectのクリーンアップの部分

になっていると思います。

useEffectで参考になったリンクたち:

useEffectの第2引数を省略しちゃいけない理由

↓何で参考にしたのか忘れました

useEffectcomponentDidUpdateの機能を備えた理由

useEffectの第2引数を設定することによるパフォーマンス向上

useLayoutEffect

useEffectのおまけとして書いていきます。
これの機能はuseEffectとほとんど同じです。使い方はuseEffectと変わらず、第1引数と第2引数があり、useEffectの使い方と意味は同じです。
唯一の違いは呼び出されるタイミングが画面が描画される前か後かです。useLayoutEffectは描画される前に呼び出されます。実際のところこれを使う場面は稀だと思います。下手に使おうとするとパフォーマンスに影響が出たりするので、初めに絶対に表示させたいものだけを実装することをお勧めします。

useLayoutEffectの参考サイト

useLayoutEffectのドキュメント

useContext

クラスコンポーネントにもあったコンテキストのフック版です。
クラスコンポーネントと比べるとちょっと実装が楽になった感じです。コンポーネントを3つ使って説明します。

UseContextSample.js
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です。

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です。

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の追跡が大変になると思います。なるべくそうならないためにコンテキストが有効になると思います。

実際にこのコードを実行すると

スクリーンショット 2023-01-19 1.54.13.png

となってUseContextSample.jsからComponentB.jsに直接値を渡せています。今回は普通の値を渡していますがstateも渡せたりできます。

useContextの公式ドキュメント

useReducer

useReduceruseStateの拡張版みたいなものです。

定義は

const [state, dispatch] = useReducer(reducer, initialArg, init);

となっています。これは公式的にはReduxをやってると馴染みがあるらしいのですが私はやってないのでよくわからないです。私自身使ったことないので触った感じで書いていきます。

普段使っている分にはstateの管理はsetStateで十分ですが、複数の値にまたがる複雑なstateロジックがある場合や、前のstateに基づいて次のstateを決める必要がある場合(公式から)やuseContextで少し触れたようにstateの受け渡しが関わる複数階層コンポーネントへの受け渡しの時に有効です(パフォーマンスの最適化)。

useReducerのよくある例だと第3引数まで使われているものが少ないので、まずは第1と第2が使われているものを使っていきます。次のコードは2で足したり割ったりする四則演算を例に書いていきます。

UseReducerSample.js
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

これを実行すると

qiita3.gif

のように2で四則演算を実装できました。

このコードでのuseReducerの説明をすると、[result, calculation]の部分はresultuseStateの部分で言うstateになり、culculationresultの結果を出す関数になってます。そしてuseReducerの第1引数はculculationが呼び出されたときに実際に実行する処理を実装する部分になります。この中で関数に渡された値から計算してresultに出していくことをしています。第2引数はresultの初期値の設定部分です。

もう少し説明していくと、第1引数で実装した関数内で(currentSResult, action)がありますが、currentSResultは名前の通り現在のresult、すなわち今のstateを表しています。actionculculationで受け取った引数が入ります。

ついでにこれをsetStateを使ったバージョンで書くと

UseStateSample2.js
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の初期化を関数で実装できるものになってます。さっきのコードを少し書き換えると

UseReducerSample.js
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

これを実行すると

スクリーンショット 2023-01-20 2.28.15.png

ほとんど変わらないのですが、初期値が0なのが嫌だったのか元々ある初期値に100を追加するように設定しています。initで受け取った引数から100を追加することで初期値に100が追加された状態の初期値が初めに表示されています。useReducerではこの関数を第3引数に入れて、第2引数をinitの引数として受け取っています。
これを使うことでReduxのようなことができるのですが公式的にはおすすめされていません。

参考にしたサイト:

useReducerの公式ドキュメント

useReducerを使う利点1

useCallback

メモ化されたコールバックを実装できます。

メモ化自体はパフォーマンスの最適化として使われますが、Reactでは少し意味が異なり、useCallbackではコンポーネント間にある依存関係から不要なレンダーを防ぐときに有効になってきます(もちろん不要なレンダーが減るのでパフォーマンスが良くなるという意味にもなります)。

比較のため、useCallbackを使わなかった例と使う例を書いていきます。

useCallbackを使わなかった例は

ComponentSample.js
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
ComponentD.js
function ComponentD(props) {
  console.log('子コンポーネントレンダー')

  return(
    <div>
      <p><button onClick={props.handleClick}>ComponentDでカウントを増やす</button></p>
    </div>
  )
}

export default ComponentD

これを実行すると

qiita4.gif

のように親と子に実装されているボタンをクリックするたびに親子のコンポーネント両方ともレンダーされています。

この時、親コンポーネントでのみの処理をしている時にもかかわらず、子コンポーネントが呼び出されています。これが不要なレンダーと考えられます。これをなくすためにuseCallbackがあります。

useCallbackを使った例は

UseCallbackSample.js
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
ComponentC.js
import { memo } from "react"

function ComponentC(props) {
  console.log('子コンポーネントレンダー')

  return(
    <div>
      <p><button onClick={props.handleClick}>ComponentCでカウントを増やす</button></p>
    </div>
  )
}

export default memo(ComponentC)

これを実行すると、

qiita5.gif

のように子コンポーネントが呼ばれず不要なレンダーが避けられるようになっています。

useCallbackを使うときの注意点として、useCallbackを使う先のコンポーネントにmemoの中にコンポーネントを入れないと機能しないことに注意してください。useCallbackの第2引数にuseEffectのような配列がありますが、これは依存配列になっていて、useeffectと同じで、配列内の値が変更されると実行されます。

ただ、useCallback自体、普通のちょっとした関数と比べると多少パフォーマンスが高いので不要な仕様は控えるようにしたほうがいいです。本当に必要な時のみ使用するようにしてください。

参考にしたサイト:

useCallbackの公式ドキュメント

useMemo

メモ化された値を実装できます。

useCallbackと使う場面がほとんど同じなので特に記載はないです。

参考にしたサイト:
useMemo公式ドキュメント

useRef

ミュータブルなrefオブジェクトを実装できます。これはミュータブルな値を.currentに保存します。実際に値を使用するときはref(変数名はなんでも良い).currentで使用します。

Reactをよく使っている人はDOMにアクセスできるやつでいつもお世話になっているものですね。

使う用途としては先ほどのDOMへのアクセスに使われることミュータブルなものとして値を保存するものとして使えます。

まずミュータブルな値を保存するものとして使う例は

UseRefSample.js
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

これを実行すると

qiita6.gif

というようになり、refの値を変更してからstateの値を変更するとrefの方の値も更新されています。ここで気づいた人もいるかもしれませんが、メモ化をしています。useRefstateのように値を変更しても再レンダリングされません。stateが更新されて再レンダリングされてもrefの値は保持されます。

そして、DOMにアクセスする例(こっちの方がいつも使われていると思います)は

UseRefSample2.js
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

これを実行すると

qiita7.gif

のようになります。.currentにDOMの情報が保持されそこからDOMの操作を行うことができます。

参考にしたサイト:
useRefのドキュメント

インスタンス変数について

オブジェクト作成の遅延方法

useImperativeHandle

子コンポーネントのメソッドをref経由で親コンポーネントで使うことができます。
公式ドキュメントでもあるようにあまりこのフックを使うこと自体はおすすめされていません。おそらく使う場面はないとは思いますが一応書いていきます。

useImperativeHandleの使い方は

useImperativeHandle(ref, createHandle, [deps])

となっていて、第1引数にはrefを入れて、第2引数にはそれぞれメソッドを定義していく場所で、第3引数はuseEffectのように依存配列を入れるところです。ここでは第2引数までの例で説明していきます。

UseImperativeHandleSample.js
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
ComponentF.js
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)

これを実行すると、

qiita8.gif

となります。reactのrefの例がフォーカスばかりなのでフォーカス以外もやりたかったのですがなんも思いつかなかったので結局フォーカスにしました。

参考にしたサイト:
useImperativeHandleの公式ドキュメント

forwardRefで参考にしたサイト

forwardRef公式ドキュメント

終わり

これくらいで終わろうと思います。他にもuseDeferredValueuseTransitionuseIdもありますが機会があれば書いていきたいなと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?