🕺

【TS】世界のナベアツに学ぶ、カスタムフック入門

6 min read 1

これはなに

Reactのカスタムフックについて、わかりやすい例えを通じてなんとなーく学べる記事です。対象読者は以下のような方々。

  • useState, useCallback等のHooksがなんとなく使える人
  • 初歩的なTypeScriptでコンポーネントが書ける/これから書きたい人
  • 世界のナベアツとReactが好きな人

Reactで世界のナベアツを作る

「簡単なカウントアプリで、3の倍数と3の付く数字は過激に表示する」というものを作ります。イメージがつきにくい人はこちらのデモを参照してください。

まず、全体像を載せておきます。

const NabeatsuCounter = () = {
  const [count, setCount] = useState(0);

  const countUp = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  //  初期状態
  const initial = count === 0;
  //  3の倍数かどうかを判定
  const isMultipleOfThree = count % 3 === 0;
  //  3がつく数字かどうかを判定
  const isNumberWithThree = Boolean(count.toString().match(/3/));
  //  0を除き、3の倍数か3のつく数字であればtrueを返す
  const isAho = !initial && (isMultipleThree || isNumberWithThree);
  
  return (
    <div>
      <h1>世界のナベアツカウンター</h1>
      <p>{count}</p>
      <div className="btn" onClick={countUp}>
        Count up!
      </div>
      <>
       {isAho ? (
         <div className="aho">Nabeatsu(Aho)</div>
       ) : (
         <div>Nabeatsu(Normal)</div>
       )}
      </>
    </div>
  )
}

次に、それぞれの部分について詳しく説明します。

  const [count, setCount] = useState(0);

  const countUp = useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  1. useStateでカウントとその更新関数を作ります。「1,2,3!!...」と読み上げが始まるので、カウントの初期値は0にしておきます。1ずつカウントアップするだけの関数もつくっておきます。
useCallbackの挙動についてメモ

本筋とは関係ありませんが、useCallbackの挙動について一応解説しておきます。useCallbackはメモ化されたコールバック関数を返すHookです。

// これでは古いカウントが参照されてしまい、何度呼んでも初期値+1を行う。
 const countUp = useCallback(() => {
    setCount(count + 1);
  }, []);

// prevStateをいちいち参照してそれに対して+1をするようにするか、
 const countUp = useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  
// 依存関係を第2引数で渡して参照を更新するようにすれば期待通りの働きをする
 const countUp = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  1. countに対していろいろな調査をして「3の倍数か3の付く数字」を判定します。「3で割った余りが0」を判定材料に用いているので、0でアホにならないようにチェックしています。
  //  初期状態
  const initial = count === 0;
  //  3の倍数かどうかを判定
  const isMultipleThree = count % 3 === 0;
  //  3がつく数字かどうかを判定
  const isNumberWithThree = Boolean(count.toString().match(/3/));
  //  0を除き、3の倍数か3のつく数字であればtrueを返す
  const isAho = !initial && (isMultipleThree || isNumberWithThree);
  1. カウント、カウントアップ用ボタン、ナベアツをそれぞれ表示します。ナベアツは三項演算子で若干わかりにくいですが、isAho = trueのときにアホなナベアツを返すようにしています。
  return (
    <div>
      <h1>世界のナベアツカウンター</h1>
      <p>{count}</p>
      <div className="btn" onClick={countUp}>
        Count up!
      </div>
      <>
       {isAho ? (
         <div className="aho">Nabeatsu(Aho)</div>
       ) : (
         <div>Nabeatsu(Normal)</div>
       )}
      </>
    </div>
  )

カスタムフックでナベアツをLogicとViewに分離する

「なぜカスタムフックを作るのか?」という質問に対し、もっともらしい回答は「ロジックを抽出して再利用可能にするため」となるのではないでしょうか。今回も世界のナベアツをロジック部分とビュー部分に分離し、ロジックの部分をuseNabeatsuHookとして抽出します。

ナベアツのロジック部分

  • 1ずつカウントアップする
  • 「3の倍数か3の付く数字」を判定する

ナベアツのビューの部分

  • 普通の顔
  • アホな顔
  • (数字)

以上を踏まえて、ロジックの部分を書き出していきます。

export function useNabeatsu(): [number, () => void, boolean] {
  const [count, setCount] = useState(0);

  const countUp = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  // 初期状態
  const initial = count === 0;
  // 3の倍数かどうかを判定
  const isMultipleOfThree = count % 3 === 0;
  // 3がつく数字かどうかを判定
  const isNumberWithThree = Boolean(count.toString().match(/3/));
  // 0を除き、3の倍数か3のつく数字であればtrueを返す
  const isAho = !initial && (isMultipleOfThree || isNumberWithThree);

  return [count, countUp, isAho];
}

中身については先程と変わらないので説明がいらないと思いますが、Hooksの大枠だけ改めて確認しましょう。

export function useNabeatsu(): [number, () => void, boolean] {
  
  // 中身を省略

  return [count, countUp, isAho];
}

返り値の[count, countUp, isAho]はそれぞれカウント・カウントアップ関数・アホ判定真偽値です。function useNabeatsu(): [number, () => void, boolean]で返り値の型を明確に定義しています。型定義によりHookの機能が把握しやすくなりますね。

useNabeatsuを作ったことにより、NabeatsuCounterはこんな感じになります。ロジックの部分がだいぶスッキリしました。

function NabeatsuCounter() {
  const [count, countUp, isAho] = useNabeatsu();

  return (
    <div className="App">
      <h1>世界のナベアツカウンター</h1>
      <p>{count}</p>
      <div className="btn" onClick={countUp}>
        Count up!
      </div>
      <>
        {isAho ? (
          <div className="aho">Nabeatsu(Aho)</div>
        ) : (
          <div>Nabeatsu(Normal)</div>
        )}
      </>
    </div>
  );
}

const [count, countUp, isAho] = useNabeatsu() を記述するだけでどの関数コンポーネントでもナベアツロジックを利用できるのはとても便利ですね!どこでもアホになれそうです!

慣例的に、カスタムフックの返り値は本家のHooksとデザインを合わせて「なし、または1つ、または2~3個の要素からなるタプル(配列)」にしておくことが多いです。

ついでにナベアツの顔の部分も明確に1つのNabeatsuコンポーネントとして分離し、最終的な全体像は以下のようになります。

import React from "react";
import "./App.css";
import { useNabeatsu } from "./useNabeatsu";

export interface NabeatsuProps {
  isAho: boolean;
}

const Nabeatsu: React.VFC<NabeatsuProps> = ({ isAho }) => (
    <>
      {isAho ? (
        <div className="aho">Nabeatsu(Aho)</div>
      ) : (
        <div>Nabeatsu(Normal)</div>
      )}
    </>
 );

function NabeatsuCounter() {
  const [count, countUp, isAho] = useNabeatsu();

  return (
    <div className="App">
      <h1>世界のナベアツカウンター</h1>
      <p>{count}</p>
      <div className="btn" onClick={countUp}>
        Count up!
      </div>
      <Nabeatsu isAho={isAho} />
    </div>
  );
}

まとめ

  • カスタムフックを使う理由は「ロジックを抽出して再利用可能にするため」
  • カスタムフックの返り値は「なし、または1つ、または2~3個の要素からなるタプル(配列)」にしておく
  • 型定義によってHookの機能が把握しやすくなる
  • ビューとロジック、意味のまとまりのあるコンポーネントをきちんと分離してコードを書くことで、すっきり読みやすくなる!

以上です。最後まで読んでくださってありがとうございました!みなさんのReactライフがよりオモロー!になることを祈っております✌️

Discussion

記事拝見いたしました。内容がとてもわかりやすかったです。
タプルの戻り値を返す際に、より把握しやすくする方法があるのでコメントさせていただきますね。

タプルにはラベルを付けることができるので、以下のような戻り値にすると、呼び出し元からそれぞれの値が何を意図しているのかが把握できるようになります。参考になさってみてください。

[count: number, countUp: () => void, isAho: boolean]

playground

ログインするとコメントできます