Subterranean Flower

ReactのカスタムHooksをカジュアルに使ってコードの見通しを良くしよう

Author
古都こと
ふよんとえんよぷよぐやま!!!

もはやReactにHooksのない生活は考えられず、私たちのReactコードの中には多数のHooksが使われています。

一方でその弊害として、使われているHooksが多すぎてコードが散らかり始めた人も多いと思います。Hooksは便利ですが粒度は小さく、プログラムの規模によっては多用しなければなりません。

そこでカスタムHooksの使用を勧めます。カスタムHooksを使うことでコードの見通しを良くすることができます。

カスタムHooksをカジュアルに使っていく

カスタムHooksというと、どちらかというとReactの中では難しい部類に入ります。主に「使い方がわからない」「公式ドキュメントが不親切」「ネットの解説が難しい」あたりが問題になるでしょう。しかし難しい機能だからと言って難しく使う必要はなく、自分の使える範囲で自由に使えばいいのではないかと思います。

カスタムHooksは一般にロジックの分離や再利用性の向上、テストの容易化などのために使われますが、実際にはもう少しカジュアルに使っても大丈夫です。難しいこと考えずにまずは「フックが増えすぎたからカスタムHooks導入してみよう」ぐらいの気持ちから始めて行きましょう。

本記事ではカスタムHooksを用いた簡単なパターンについて、いくつか紹介します。読んでいくうちにその魅力に気づいていただければと思います。

カスタムHooksの基本

Hooksの基本的な知識については最近Reactを始めた人向けのReact Hooks入門をご覧ください。

ReactではカスタムHooksを作ることで、既存のHooksに囚われない自由な処理を取り扱えます。カスタムHooksの作り方は簡単で、ただ関数を定義するだけです。決まり事は名前を useXXX にする、ただそれだけです。これにも強制力があるわけではないですが、ReactのデフォルトHooksがすべて useXXX なのでそれに合わせておきましょうというだけの話です。

const useHello = () => {
  return 'Hello';
};

しかし上の定数を返すだけの例は本当にHooksなのかというと怪しいです。普通は他のHooksと組み合わせて使います。カスタムHooksの内部では他のHooksを使うことが可能です。

const useCount = () => {
  const [count, setCount] = useState(0);
  const countUp = () => setCount((c) => c+1);
  
  return [count, countUp];
  // TypeScriptの場合はas constをつける
  // return [count, countUp] as const;
}

このuseCountというHooksでは内部でuseStateを使っており、状態値を保持することができます。返却値はcount値そのままと、countを増加させるcountUp関数です。

使い方は普通のHooksと同じです。

const App = () => {
  const [count, countUp] = useCount();
  return <div onClick={countUp}>{count}</div>
};

カスタムHooksのマナー

カスタムHooksには制約がありません。自由な使い方をしても動作に支障が出ることはなく、何かしらのルールを守る必要はありません。

しかし一般的なマナーのようなものが暗黙に存在し、これを破ると使用者側にちょっとびっくりされます。そのマナーというのは「デフォルトのHooksにスタイルを合わせる」です。具体的には以下の2つになります。

  • 戻り値は無し、1個の値、2個のタプルのいずれかである

    • 無しはuseEffect、1個はuseRef、2個のタプルはuseState、が代表的
    • 4個とか5個とかになってきたら分割を考えてみましょう
  • 使用者側で再評価の制御をしたい場合は依存配列を使う

    • useEffectの第二引数と同じようにする

どちらも簡単なマナーなので、少し意識してみると綺麗なカスタムHooksが作れるかもしれません。

複数のHooksをまとめる

例えば時計アプリを作りたいとします。素直に作れば以下のようになるでしょう:

export const App = () => {
  const [date, setDate] = useState(new Date());

  // 毎秒更新する
  useEffect(() => {
    const timer = setInterval(() => {
      setDate(new Date());
    }, 1000);

    return () => clearInterval(timer);
  }, [setDate]);

  return <div>{date.toLocaleTimeString()}</div>
};

このコードには特に問題はなく、動作もします。しかし少々不格好というか、将来的な機能追加にともなってどんどん膨れ上がっていくことが予想できます。

問題の根本としては、1つの機能に対して2つのHooksを使っているところにあります。また、内部的なロジックもベタ書きされており、少し「細かいコード」かなという印象を受けます。

このDateの更新周りのHooksをカスタムHooksに押し込めてしまいます。

const useCurrentDate = (interval) => {
  const [date, setDate] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setDate(new Date());
    }, interval);

    return () => clearInterval(timer);
  }, [setDate]);

  return date;
};

そしてこのHooksをコンポーネント側で使います。

export const App = () => {
  const date = useCurrentDate(1000);
  return <div>{date.toLocaleTimeString()}</div>
};

びっくりするほどスッキリしましたね!このようにカスタムHooksを使うことで、複雑に絡み合ったHooksをひとつのHooksにまとめあげることができます。

「ひとつのコンポーネントにHooksが10個とか並んでて、しかもどれがどれに依存しているのかわからない…」という方はカスタムHooksを使用してみてはどうでしょう。

複雑なイベント制御を隠蔽する

Reactのコードはシンプルに保つことこそ美徳です。しかし複雑なイベントを扱う場合はそうも言ってられません。

例えば「マウスの位置を取得したい。なおかつイベントはthrottlingする」などと考えだすと、コードの量はすさまじいことになります。このとき useMousePosition というフックを作って、そこに複雑な処理を押し込めれば物事はシンプルになりそうです。

const useMousePosition = (interval = 0) => {
  const [position, setPosition] = useState({x: 0, y:0});

  useEffect(() => {
    let lastCalled;

    const handleMouseMove = (event) => {
      const currentTime = performance.now();
      if(lastCalled && currentTime - lastCalled < interval) {
        return;
      }

      setPosition({x: event.pageX, y: event.pageY});
      lastCalled = currentTime;
    };

    document.addEventListener('mousemove', handleMouseMove);
    return () => document.removeEventListener('mousemove', handleMouseMove);
  }, [interval, setPosition]);

  return position;
};

useMousePoition フックは引数で受け取ったインターバルでスロットリングされる、マウス座標取得処理です。このフックを使うには、ただ以下のようにするだけです:

export const App = () => {
  const {x, y} = useMousePosition(50);
  return <div>Pos: {x}, {y}</div>;
};

実にシンプルですね。処理が複雑になればなるほどカスタムHooksは効果を発揮します。

操作を制限する

Hooksは汎用的な機能であり、使用側に任意の操作を許します。しかし時としてそれが邪魔になることがあります。そういった場合は通常のHooksをカスタムHooksを使って隠してやることで操作を制限することができます。

例えば以下は非同期処理の結果を変数に格納するコードです:

export const App = () => {
  const [data, setData] = useState();

  useEffect(() => {
    const timer = setTimeout(() => setData('Hello'), 1000);
    return () => clearTimeout(timer);
  }, [setData]);

  return <div>{data}</div>;
};

しかしこのとき setData はコンポーネント内に無防備に露出しています。つまりデータを変更しようと思えば App コンポーネント内ならどこでもできてしまいます。問題になることは少ないのですが、可能であれば隠してしまいたい場合もあると思います。

カスタムHooksを使うことで setData を隠してしまうことができます。非同期部分を useAsyncData フックとして切り出してみます。

const useAsyncData = (promise) => {
  const [data, setData] = useState();

  useEffect(() => {
    promise.then((result) => setData(result));
  }, [promise]);

  return data;
};

あとは使うだけですね。

const timeout = async (fn, delay) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(fn()), delay);
  });
};

export const App = () => {
  const data = useAsyncData(timeout(() => 'Hello', 1000));

  return <div>{data}</div>;
};

カスタムHooksを通して利用することで setData を隠してしまって、関係のない場所からは触れないようにしました。

さいごに

カスタムHooksというと複雑な事情を備えた高度な機能だと捉えがちです。もちろん結合の緩和であるとか抽象的なインターフェイスの提供などに使うのが一番効果を発揮するのですが、まずはカジュアルに、ちょっとした処理をまとめるところから始めてみませんか?

慣れきたらより難しい使い方もできるようになりますし、なによりカジュアルな使い方でも効果は大きいです。いきなり100%活用しようとするのではなく、40%ぐらいを目指せばいいと思います。それで十分です。

Reactには今後も様々な機能が増えると思いますが、怖がらず、軽く触れてみるところから始めてみましょう。きっと何かのためになるはずです。