🦁

render hooks パターンの素振り

2022/06/02に公開

以下の記事で紹介されている「render hooks パターン」がすごく良いなと思ったので、実際に業務で利用した構成を元に実装してみました。

https://engineering.linecorp.com/ja/blog/line-securities-frontend-3/

最終的な実装

画像ファイルを指定すると、直下に画像のプレビューを表示するコンポーネントを作りました。

こちらを元に解説していきます。

パターンを使わない実装

以下はカスタムフックを利用せずに実装した例です。
<input type="file" />に入力があったのをhandleInput()で画像を data URL に変換しています。

src/app.tsx
import { useState } from "react";

export default function App() {
  const [value, setValue] = useState<string | null>(null);

  const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
    const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = (event: ProgressEvent<FileReader>) => {
      if (typeof event?.target?.result === "string") {
        setValue(event.target.result);
      }
    };
    reader.readAsDataURL(file);
  };

  return (
    <div className="App">
      <input type="file" accept="image/*" onChange={handleInput} />
      {value && <img src={value} alt="preview" />}
    </div>
  );
}

<input> に render hooks パターンを適用した実装

まずは、<input type="file" />をカスタムフックから提供するようにしてみます。

src/use-image-file-input.tsx
import { useState } from "react";

export default function useInputImageFile() {
  const [imageDataUrl, setImageDataUrl] = useState<string | null>(null);

  const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
    const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
    if (!file) {
      return;
    }
    const reader = new FileReader();
    reader.onload = (event: ProgressEvent<FileReader>) => {
      if (typeof event?.target?.result === "string") {
        setImageDataUrl(event.target.result);
      }
    };
    reader.readAsDataURL(file);
  };

  const imageFileInput = () => 
  <input type="file" accept="image/*" onChange={handleInput} />

  return {
    imageDataUrl,
    imageFileInput,
  }
}

app.tsx に記載していた画像ファイル取得後のロジックがほとんどカスタムフックに移動しました。この時、app.tsx は以下のようになります。

src/app.tsx
import { useImageFileInput } from "./use-image-file-input";

export default function App() {
  const { imageDataUrl, ImageFileInput } = useImageFileInput();

  return (
    <div className="App">
      <ImageFileInput />
      {imageDataUrl && <img src={imageDataUrl} alt="preview" />}
    </div>
  );
}

ロジックがカスタムフックに凝集されたことにより、とてもシンプルになりました。ImageFileInputが通常のuseStateの set 関数のように振る舞うので、使いやすいというのも気に入っています。

画像プレビューにも render hooks パターンを適用した実装

画像取得した後に表示するプレビューもカスタムフックの責務にしてしまいましょう。useImageFileInputuseImageFileにリネームし、プレビューのコンポーネントも提供するようにします。

src/use-image-file.tsx
  import { useState } from "react";

- export function useImageFileInput() {
+ export function useImageFile() {
    const [imageDataUrl, setImageDataUrl] = useState<string | null>(null);

    const handleInput = (changeEvent: React.ChangeEvent<HTMLInputElement>) => {
        const file = changeEvent.target.files ? changeEvent.target.files[0] : null;
        if (!file) {
        return;
        }
        const reader = new FileReader();
        reader.onload = (event: ProgressEvent<FileReader>) => {
        if (typeof event?.target?.result === "string") {
            setImageDataUrl(event.target.result);
        }
        };
        reader.readAsDataURL(file);
    };

    const ImageFileInput = () => (
        <input type="file" accept="image/*" onChange={handleInput} />
    );

+   const ImagePreview = () =>
+       imageDataUrl ? <img src={imageDataUrl} alt="preview" /> : <></>;

    return {
        imageDataUrl,
        ImageFileInput,
+       ImagePreview
    };
  }

app.tsx には分岐も無くなり、よりシンプルになりました。

src/app.tsx
- import { useImageFileInput } from "./use-image-file-input";
+ import { useImageFile } from "./use-image-file";

  export default function App() {
-   const { imageDataUrl, ImageFileInput } = useImageFileInput();
+   const { ImageFileInput, ImagePreview } = useImageFile();

    return (
      <div className="App">
        <ImageFileInput />
-       {imageDataUrl && <img src={imageDataUrl} alt="preview" />}
+       <ImagePreview />
      </div>
    );
  }

カスタムフックとして画像入力、プレビュー出力を提供することで再利用性が高まります。例えば画像が未選択の際に「画像が入力されていません」とプレビューに表示したい場合はカスタムフック内のプレビューの定義を変更することで、利用しているコンポーネント全てに反映させることができます。他にも選択画像のキャンセルボタンを提供するなど、利用しやすいカスタムフックが作れそうです。

まとめ

画像ファイルの入出力を題材に、render hooks パターンの例を作成してみました。このパターンの肝は、UIコンポーネントとそれに関連するステートの凝集性を高めることだと個人的に理解しています。表現の幅が広がるテクニックだと感じており、今後も要所で利用していくつもりです。

コードの内容は以下のリポジトリにまとめてあります。

https://github.com/MssKnd/render-hooks-demo

GitHubで編集を提案

Discussion