LoginSignup
1
2

Reactの関数コンポーネントとHooks #2

Last updated at Posted at 2021-03-21

Part1の続きです。

メモ化フック

同じ結果を返す処理に関しては初回のみ処理を実行しておき、2回目以降は前回の処理結果を呼び出すことで毎回同じ処理を実行させない。

プログラミングではメモ化と呼ばれる高速化テクニックのひとつです。

これをReact Hooks上で簡単に利用できるのがmemouseMemouseCallbackです。パフォーマンス向上のために使用します。(必要のない再レンダリング、再計算を抑制する為なので、使わなくても動作はします。)

コンポーネントのメモ化

そもそも再レンダリングが発生するタイミング:
(1) propsや内部状態が更新された時
(2) 親コンポーネントが再レンダリングされた時
(3) コンポーネント内で参照してるContextの値が更新された時

特に問題なのは(2)で、親コンポーネントが再レンダリングされると無条件に子コンポーネントでも再レンダリングが発生します。
このため、上位コンポーネントで再レンダリングが発生すれば、それ以下の階層のコンポーネント全てでも再レンダリングが発生してしまい、パフォーマンス的によくありません。

この再レンダリングの伝播を止めるためにメモ化コンポーネントを使います。
メモ化されたコンポーネントはprops, contextの値が変化しない限り再レンダリングされず、親コンポーネントによる再レンダリングが発生しません。

//通常の関数コンポーネント
const Fizz = (props => {
  const { isFizz } = props
  return <span>{isFizz ? "Fizz" : ""}</span>
})
//メモ化した関数コンポーネント
const Buzz = React.memo(props => {
  const { isBuzz } = props
  return <span>{isBuzz ? "Buzz" : ""}</span>
})

export const Parent = () => {
  const [count, setCount] = useState(1)
  const isFizz = count % 3 === 0
  const isBuzz = count % 5 === 0
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}> +1 </button>
      <Fizz isFizz={isFizz} />
      <Buzz isBuzz={isBuzz} />
    </>
  )
}

このコンポーネントを実行すると、countが増加するたびにParentとFizzは再レンダリングされますが、Buzzはメモ化してるのでisBuzzが変化した時だけ再レンダリングされます。

このように子コンポーネントをメモ化することで不要な再レンダリングを抑制することができます。

引用元:
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発/技術評論社

useMemo: 値のメモ化

//基本形、第2引数に入れた依存配列の値のいずれかが変化した場合にのみ再計算が実行される
const memorizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

//実際のサンプル countが更新された時だけ再計算が実行される。そうでない場合はメモ化(保存)された値を返します。
const [count, setCount] = useState(0)
const hoge = useMemo(() => count * 2, [count])

useCallback: 関数のメモ化

親コンポーネントからコールバック関数を受けた子コンポーネントは関数の内容が同じだとしても、親コンポーネントの再レンダリングの影響を受けます。

それを防ぐために、useCallbackを使います。useCallbackでメモ化したコールバック関数を Propsで渡せば子コンポーネントの不要な再レンダリングをスキップできます。

//基本形、第2引数に入れた依存配列の値のいずれかが変化した場合にのみ変化します
const memorizedCallback = useCallback(
  () => {
    doSomething(a, b);
  }, [a, b]);

使い方の注意点 1
・useCallbackでメモ化したコールバック関数を、それを生成したコンポーネント自身で利用はできない(つまりPropsで子に渡す場合のみ有効)
・親コンポーネントからuseCallbackを渡しても、子コンポーネント自体がReact.memoでメモ化されている必要がある(つまり、useCallbackとReact.memoはセットで使う)

↑の注意点を守らないと意味のないuseCallbackになってしまいます。

以下がその注意点を守ったサンプルになります。useCallback, React.memoを使ってなくても動作に変わりはありませんが、入力の度にconsole.log('InputBox is rendered')が呼ばれます。

この程度の実装なら大した影響はないですが、コンポーネントが増えて大規模になってきたりするとパフォーマンスに悪影響をもたらす原因になるので、積極的に取り入れていきたいと思います。

//親コンポーネント
const Parent = () => {
  const [ input, setInput ] = useState("")
  //コールバックをメモ化
  const onChange = useCallback(
    (e) => {
      setInput(e.target.value)
    },
   []
  );
  return <InputBox onChange={onChange} />
}

//コールバックがメモ化されていても、
//子コンポーネント自体をメモ化しないと親の再レンダリングの影響受ける
const InputBox = React.memo(props => {
  const { onChange } = props
  console.log('InputBox is rendered')
  return <input type="text" onChange={onChange}/>
})

使い方の注意点 2
useMemouseCallbackもuseEffectと同じく第2引数に何も入れなかった場合、新しい値がレンダーごとに毎回計算されてしまい、これらを使う意味がなくなるので、第2引数に依存する値(監視する値)もしくは空配列は必須です。


useContext

コンポーネントツリー上、親子関係にない(=ツリー上離れたところにいる)コンポーネント間で同じ値を共有する事ができる。
(propsでバケツリレーしないでも値の受け渡しができるReduxの様な使い方が可能です。)

//Provider(親)
export const Context = createContext({});
const hoge = { name: 'taro yamada' };

return (
  <Context.Provider value={hoge}>
    <ChildComponent />
  </Context.Provider>
)

//Consumer(子)
import { Context } from "...";

const value = useContext(Context);
//valueの中に{ name: 'taro yamada' }が入る

Consumer(子)側でデータを受け取るだけなら上記の様に簡単にできます。

単純にデータを渡すだけでなく、親からはコールバックを渡して、子の方からデータを送ってもらう事も可能です。

//Provider(親) addToData関数で子から値を受け取りStateに保存する
return (
  <Context.Provider sendData={hoge => addToData(hoge)}>
    <ChildComponent />
  </Context.Provider>
)

//Consumer(子) sendDataを使って親にデータを送る
import React , { useContext } from 'react';
import { Context } from "...";

const sendData = useContext(Context);
sendData(hogeData);

このuseContextを拡張すると、Redux無しでStoreを持つ事ができるのですが、それは長くなるので別の記事で紹介します。

useRef

ざっくりいうとDOMにアクセスするするためのHooksです。

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>
    </>
  );
}

普通に単一のDOMを参照したい場合はこの様に簡単です。useRefでref オブジェクトを生成し、それをDOMに渡せば参照する事ができます。

しかし、配列データから複数個のDOMを生成してる場合はちょっと工夫が必要です。

こちらの記事にそのやり方が書いてあるので参照してください。


まとめ

関数コンポーネントのメリットは、constructor, renderなどが不要で記述量を減らせて、慣れれば圧倒的に楽に書けるのでReactプロジェクトでこっちが主流になる理由が良くわかりました。

1
2
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
1
2