0
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 3 years have passed since last update.

今更ながらReactのHooksを使ってみた

Last updated at Posted at 2020-03-29

はじめに

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の引数は初期値。

loading.jsx
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の変化を検知して処理を行う場合に使う。

loading.jsx
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で管理して、多重ロードを避けるために書いたコードが以下。

loading.jsx
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のままでした。
なのでこうしました。

loading.jsx
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などの記述量の多さが苦手で、できれば避けたいと思っていましたが、避けられない場面が出てきました。

サーバから取得した結果を順々に配列に追加していくような、以下のコードを書いてみました。

loading.jsx
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>
    )
}

これでうまくいくかと思いきや、追加したデータが消えていたりする。。。
今試してみたサンプルコードは以下。

sample.jsx

    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の出番。

sample.jsx
    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を踏まえて処理したい場合に有効
0
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
0
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?