LoginSignup
2
2

More than 1 year has passed since last update.

第 9 章 Hooks 、関数コンポーネントの合体強化 パーツ メモ

Posted at

コミュニティによって普及した HOC

HOC とはコンポーネント を引数に取り、戻り値としてコンポーネントを返す関数のこと。

render props

render props は必 ずしも render という名前の props を使う必要はなくてね。そのコンポーネントが自身のレンダリン
グのために使うコンポーネントの props は、何だろうと技術的には render props と呼ぶの

HOC より render props のほうが優れていると主張する理由は次のようなもの
・HOC のように props の名前の衝突が起こりづらく、起こったとしてもコードから一目瞭然
・TypeScript 使用時、props の型合成が必要ない
・どの変数が機能を注入するための props として親コンポーネントに託されたのかがコードから 判別しやすい
・コンポーネントの外で結合させる必要がなく、JSX の中で動的に扱うことができる

ついに Hooks が登場する

Hooks は公式が新たに React の機能として提供したもの
Hooks はコンポーネントにロジックを抱えた別のコンポーネントをかぶせるのではなく、コンポーネント システムの外に状態やロジックを持つ手段を提供した

Hooks を使えば、状態を持ったロジックを完全に任意の コンポーネントから切り離し、それ単独でテストしたり、別のコンポーネントで再利用することが 簡単にできるようになる。コンポーネントの階層構造を変えることなしに、状態を伴った再利用可 能なロジックを追加できるというのが画期的だった

Hooks がコンポーネントシステムの外に状態やロジックを 持つしくみを提供したことによって、ほぼ関数コンポーネントだけでアプリケーションが作れるよ うになったの。というか Hooks は関数コンポーネント内でしか使えない

2021 年 3 月現在 getSnapshotBeforeUpdate、getDerivedStateFromError、componentDidCatch の 3 つのライフサイクルメソッドに相当する機能は Hooks では提供されてない
ただ将来的には 追加される予定

新しく作るコンポーネン トは Hooks と関数コンポーネントで作ることが推奨されている

Hooks で state を扱う

State Hook といってクラスコンポーネントの state に相当するものを関数コンポーネントでも使えるようにする機能
useState という関数を 使う

const [count, setCount] = useState(0); 
setCount(100);
setCount(prevCount => prevCount + 1);

useState は戻り値として state 変数とその state 更新関数をタプルとして返す
だから上の ように分割代入で受け取る
配列の分割代入と同様なのでもちろん、state 変数とその更新関数 の名前は好きなものに設定できる

useState(INITIAL_VALUE) のように引数を渡すと、その値が state 変数の初期値として設定される

import { VFC, useState } from 'react';
import { Button, Card, Statistic } from 'semantic-ui-react';
import './Counter.css';

const Counter: VFC = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((c) => c + 1);
  const reset = () => setCount(0);

  return (
    <Card>
      <Statistic className="number-board">
        <Statistic.Label>count</Statistic.Label>
        <Statistic.Value>{count}</Statistic.Value>
      </Statistic>
      <Card.Content>
        <div className="ui two buttons">
          <Button color="red" onClick={reset}>
            Reset
          </Button>
          <Button color="green" onClick={increment}>
            +1
          </Button>
        </div>
      </Card.Content>
    </Card>
  );
};

export default Counter;
const plusThreeDirectly = () => [0, 1, 2].forEach((_) => setCount(count + 1));
const plusThreeWithFunction = () => [0, 1, 2].forEach((_) => setCount((c) => c + 1));

state 変数はそのコンポーネントのレンダリングごとで一定
よって plusThreeDirectly() はそ のレンダリング時点での count が 0 だったとしたら、それを 1 に上書きする処理を 3 回繰り返すこ とになる
だから state 変数を相対的に変更する処理を行うときは、前の値を直接参照・変更 するのは避けて必ず setCount((c) => c + 1) のように関数で書くべき

Hooks の呼び出しはその関数コンポーネントの論理階層の トップレベルでないといけない

Hooks で副作用を扱う

Effect Hook の使い方

副作用を扱う Hooks API を Effect Hook という

コンポーネントの『副作用』とは
ネットワークを介したデータの取 得やそのリアクティブな購読、ログの記録、リアル DOM の手動での書き換えといったもの

Effect Hook とは、props が同一であってもその関数コンポーネントの出力内容を変えてしまうような処理をレンダリングの タイミングに同期させて実行するための Hooks API のこと

useEffect という関数を使う

const SampleComponent: VFC = () => { 
 const [data, setData] = useState(null); 
 
 useEffect(() => {
  doSomething();

  return () => clearSomething(); 
 }, [someDeps]);
 
};

まず useEffect は第 1 引数として、引数を持たない関数を受け取る。この関数 の中身が任意のタイミングで実行される
useEffect へ第 1 引数として渡す関数がその戻り値として任意の関数を返すように
しておくと、そのコンポーネントがアンマウントされるときにその戻り値の関数を実行してくれる

useEffect の第 2 引数、ここには変数の配列を渡せるようになってる
この配列の中 に格納された変数がひとつでも前のレンダリング時と比較して差分があったときだけ、第 1 引数の 関数が実行される
この第 2 引数のことを 依存配列(dependencies array)ともいう

第 2 引数は省略可能
ただし依存配列が渡されなかった場合、レンダリングごとに毎回第 1 引数 の関数が実行されることになる
空配列 [] を渡すと、初回のレンダリング時にのみ第 1 引数の関数が実行される

import { VFC, useEffect, useState } from 'react';
import { Button, Card, Icon, Statistic } from 'semantic-ui-react';
import './Timer.css';

const Timer: VFC<{ limit: number }> = ({ limit }) => {
  const [timeLeft, setTimeLeft] = useState(limit);
  const reset = (): void => setTimeLeft(limit);
  const tick = (): void => setTimeLeft((t) => t - 1);

  useEffect(() => {
    const timerId = setInterval(tick, 1000);

    return () => clearInterval(timerId);
  }, []);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  useEffect(() => {
    if (timeLeft === 0) setTimeLeft(limit);
  });

  return (
    <Card>
      <Statistic className="number-board">
        <Statistic.Label>time</Statistic.Label>
        <Statistic.Value>{timeLeft}</Statistic.Value>
      </Statistic>
      <Card.Content>
        <Button color="red" fluid onClick={reset}>
          <Icon name="redo" />
          Reset
        </Button>
      </Card.Content>
    </Card>
  );
};

export default Timer;

Effect Hook とライフサイクルメソッドの相違点

  1. 実行されるタイミング
  2. props と state の値の即時性
  3. 凝集の単位
1. 実行されるタイミング

useEffect が初回実行されるのは、最初のレンダリングが行われてその内容がブラウザ に反映された直後。コンポーネントはまず初期値でレンダリングされた後、あらためて副作用が反 映された内容で再レンダリングされる

2. props と state の値の即時性

クラスコンポーネントのメンバーメソッドで参照される props や state は常に最新の値だけど、そ のため負荷のかかる処理を伴う UI ではタイムラグを考慮する必要がある。処理に時間がかかって レンダリングが追いつかない状態で UI を操作をすると、新しすぎる props や state の値が想定外の 挙動を起こすことがあるの。
いっぽう関数コンポーネント内部におけるそれらはレンダリングのタイミングで固定されている ため、同様の問題は起きない。
ブログ記事『関数コンポーネントはクラスとどうちがうのか?』

3. 凝集の単位

Effect Hook はライフサイクルメソッドに比べて機能的凝集度が高い
機能的凝集度が高いということは、同じ機能が分散して記述されないためコードの可読性が高い のはもちろん、機能によってまとまったロジックをコンポーネントから切り離して再利用しやすい ということでもある

Hooks におけるメモ化を理解する

メモ化とはプログラム高速化の手法のこと

import { VFC, useEffect, useMemo, useState } from 'react';
import { Button, Card, Icon, Statistic } from 'semantic-ui-react';
import { getPrimes } from 'utils/math-tool';
import './Timer.css';

type TimerProps = {
  limit: number;
};

const Timer: VFC<TimerProps> = ({ limit }) => {
  const [timeLeft, setTimeLeft] = useState(limit);
  const primes = useMemo(() => getPrimes(limit), [limit]);
  const reset = () => setTimeLeft(limit);
  const tick = () => setTimeLeft((t) => t - 1);

  useEffect(() => {
    const timerId = setInterval(tick, 1000);

    return () => clearInterval(timerId);
  }, []);

  useEffect(() => {
    if (timeLeft === 0) setTimeLeft(limit);
  }, [timeLeft, limit]);

  return (
    <Card>
      <Statistic className="number-board">
        <Statistic.Label>time</Statistic.Label>
        <Statistic.Value
          className={primes.includes(timeLeft) ? 'prime-number' : undefined}
        >
          {timeLeft}
        </Statistic.Value>
      </Statistic>
      <Card.Content>
        <Button color="red" fluid onClick={reset}>
          <Icon name="redo" />
          Reset
        </Button>
      </Card.Content>
    </Card>
  );
};

export default Timer;

useMemo を使って計算結果をコンポーネン トシステムの外に保存しておく

useMemo は useEffect と同じインターフェース
第 1 引数に実行したい関 数、第 2 引数にその依存配列を渡してる

useMemo が関数の実行結果をメモ化する Hooks API だったのに対して、useCallback は関数定義そ のものをメモ化するためのもの

メモ化はパフォーマンスの最適化以外に、依存関係を適切化して不要な再レンダリングを避けるためにも用いられることがある

useRef は useState とちがって値の変更がコンポーネントの再レンダリングを発生させない

Custom Hook でロジックを分離・再利用する

コンポーネントから Hooks のロジックを切り出したものを『Custom Hook』って呼ぶ
Custom Hook 関数の名前の頭に『use』をつける

import { VFC } from 'react';
import { Button, Card, Icon, Statistic } from 'semantic-ui-react';
import useTimer from 'hooks/use-timer';
import 'components/Timer.css';

const Timer: VFC<{ limit: number }> = ({ limit }) => {
  const [timeLeft, isPrime, reset] = useTimer(limit);

  return (
    <Card>
      <Statistic className="number-board">
        <Statistic.Label>time</Statistic.Label>
        <Statistic.Value className={isPrime ? 'prime-number' : undefined}>
          {timeLeft}
        </Statistic.Value>
      </Statistic>
      <Card.Content>
        <Button color="red" fluid onClick={reset}>
          <Icon name="redo" />
          Reset
        </Button>
      </Card.Content>
    </Card>
  );
};

export default Timer;
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getPrimes } from 'utils/math-tool';

const useTimer = (limit: number): [number, boolean, () => void] => {
  const [timeLeft, setTimeLeft] = useState(limit);
  const primes = useMemo(() => getPrimes(limit), [limit]);
  const timerId = useRef<NodeJS.Timeout>();
  const tick = () => setTimeLeft((t) => t - 1);

  const clearTimer = () => {
    if (timerId.current) clearInterval(timerId.current);
  };

  const reset = useCallback(() => {
    clearTimer();
    timerId.current = setInterval(tick, 1000);
    setTimeLeft(limit);
  }, [limit]);

  useEffect(() => {
    reset();

    return clearTimer;
  }, [reset]);

  useEffect(() => {
    if (timeLeft === 0) reset();
  }, [timeLeft, reset]);

  return [timeLeft, primes.includes(timeLeft), reset];
};

export default useTimer;
2
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
2
2