React Hooks その2

前回の記事で基本的なReact Hooksについて復習しました。

今回はその続きとなります。

useReducer

useReduceruseStateと同じく状態管理を行うためのHookです。

以下のような構文になっております。

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

stateが管理したい状態、dispatchreducerを呼び出すための関数です。実際の状態変更はreducerが行います。

stateの初期値はinitialArgとなります。

reducerは以下のような関数です。

const reducer = (state, action) => newState;

stateactionを引数に取り、新しい状態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の引数actiontypeによって処理を切り替えているのがわかります。

これを実際に使ってみると、

hogeボタンを押したらhogeの数値が、fugaボタンを押したらfugaの数値が、hogefugaボタンを押したら両方の数値がインクリメントするコンポーネントができました。

最初に述べた通りuseReduceruseStateと同様に状態管理を行うための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.typeresetの場合は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>
  )
}

superHeavyFunctionuseMemoでラップすると、

hogeを押した時だけコンソールにhogeと表示され、fugaを押した時には何も出なくなりました。

useRef

useRefcurrentプロパティに引数で渡された値を持つオブジェクトを返します。

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を使って最適化できるようにしたいと思います。