スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

カスタムフックを使いながら、バーチャル背景生成ツールを開発した話

🦓 < こんにちは!

先日、完全なReact初心者が社内用の「バーチャル背景生成ツール」を開発・デプロイしました!

説明するより、みてもらったほうが早いと思うので、完成したものの動画を貼っておきます 👇

シンプルなツールですが、作る過程でさまざまな学びがあり、その中でもカスタムフックについて残そうと思います!

是非読んでいってくれると嬉しいです 🙇‍♂️

使用技術と構成

先にさっと、使用技術の詳細と構成を書きます

  • React Router
  • Chakra UI
  • Vite

ファイル構成

packages/client/src
├─ assets/        — 画像・フォントなど
├─ components/    — 共通Reactコンポーネント
├─ constants/     — 各種定数
├─ hooks/         — カスタムフック
├─ pages/         — 画面コンポーネント
├─ test/          — テスト類
├─ types/         — 型定義
├─ app.tsx        — ルートApp
├─ main.tsx       — DOMエントリ
├─ router.tsx     — ルーティング設定
└─ vite-env.d.ts  — Vite型宣言

ViewとControllerを分ける??

自分で実装してみたり、Copilotくんに頼ったりしながら実装してました。

しかしいざレビューをしてもらう時に、

「見た目のコンポーネントにコントローラー相当の実装を詰め込まない方がいい!」

と言われました。

一体どういうことだ...? となりつつも、教えてもらいました!!

React は厳密な MVC ではないものの、「UI(View)に寄せた部分」と「ロジック(Controller 的な振る舞い)」を意識的に切り分けると読みやすくなるケースが多いとのこと。

ずばり、今回のケースではUIとロジックを分けると書きやすかった、つまりTSXでは表示のみに徹しカスタムフックでコンポーネントのロジックを受け持たせるのがよい構成だったのです。

これだけだとピンとこないので、実例を見てみましょう

分ける前

これを

import { useEffect, useState, type ChangeEvent } from "react"

export function TestComponent() {
    const [inputText, setInputText] = useState<string | null>(null);
    const [response, setResponse] = useState<string | null>(null);

    useEffect(() => {
        // 複雑な処理
        setResponse(`inputは${inputText}だよ!`)
    }, [inputText])

    const onChange = (event: ChangeEvent<HTMLInputElement>) => {
        setInputText(event.currentTarget.value);
    }

    return (
        <div>
            <input type='text' placeholder='Enter some text' onChange={onChange} />
            {response ? <p>{response}</p> : <></>}
        </div>
    )
}

分けた後

こうする!👇 (カスタムフック使用)

import { type ChangeEvent } from "react"
import { useResponder } from "../hooks/useResponder";

export function TestComponent() {
    const { setInputText, response } = useResponder();

    const onChange = (event: ChangeEvent<HTMLInputElement>) => {
        setInputText(event.currentTarget.value);
    }

    return (
        <div>
            <input type='text' placeholder='Enter some text' onChange={onChange} />
            {response ? <p>{response}</p> : <></>}
        </div>
    )
}
import { useEffect, useState } from "react";

export function useResponder() {
    const [inputText, setInputText] = useState<string | null>(null);
    const [response, setResponse] = useState<string | null>(null);

    useEffect(() => {
        if (!inputText) {
            setResponse(null);
            return;
        }
        setResponse(`inputは${inputText}だよ!`)
    }, [inputText])

    return {
        setInputText,
        response
    };
}

分けておくと、見た目のコンポーネントはスッキリ保てます。 特に機能が増えてロジックが膨らんできたときに効果を発揮します 😎

カスタムフックの便利なところ

1. コードの見通しがよくなる/理解しやすくなる

やっぱり、これ大事ですよね。

  • UIを変えてもフックのインターフェースさえ守れば影響範囲を抑えられる。
  • ロジックもフック内に閉じ込めておけば、状態管理の手戻りを減らしやすい。

責務を分けることでUIとロジックの境界が明確になり、余計なバグを減らしながら開発できるのが本当にいいなと感じました 👀

2. ロジック・状態を使いまわせる

一度フックに必要なロジックをしっかり書けば、他のコンポーネントでそのロジックと状態管理を使いまわせる!

例えば、他のコンポーネントで書いた

  • 入力値の設定
  • ファイル生成
  • ダウンロード

というコードがあったとして、それを何度も貼り付けなくていい=保守性が上がる

しかも、コンポーネント側では入力値を設定するだけなので、表示周りのコードの見通しも保ちやすいです。

3. テストがしやすくなる

テストをするときに、全て一つのファイルに詰め込むと、 結果として、ビューのモックを作ったりあらゆる値をセットしないとテストできない!っていうことがありえるかも知れません。

フックにロジックを分離すれば、フック単体にフォーカスしたテストを書きやすくなりますし、最小限のモックで済ませられるケースも増えます。

これもとても大きい恩恵だと思いました

終わりに

今回、いろんな初見の技術を使いつつ、様々なことを学びながら実装ができました!!

これからもどんどん新しい概念や設計の基礎を学び、より良い品質で開発ができるように頑張ります〜