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

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

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

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

```react: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のままでした。
なのでこうしました。

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

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

```react: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>
    )
}
```

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

```react: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)
        }
    }, [])
```

結果はこう。

```console
-----from-----
0
[]
-----to-----
-----from-----
1
[99]
-----to-----
```

前のstateを踏まえて何か処理する場合、useRefで参照を持っていても不十分だったようです。
そこでuseReducerの出番。

```react: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 })
        }
    }, [])
```

結果はこう。

```console
-----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を踏まえて処理したい場合に有効