ReactのuseEffect・useCallbackと非同期処理のメモ化戦略:SVG生成を例に徹底解説!

Reactで開発をしていると、副作用(side effects)をどう管理するかは非常に重要なテーマの1つです。API呼び出し、イベントリスナーの登録、ローカルストレージの読み書きなど、コンポーネントの描画とは直接関係しない処理をうまく扱わないと、バグやパフォーマンス低下につながります。

この記事では、ReactのuseEffectやuseCallbackの正しい使い方を、OGP画像(Open Graph Protocol image)の生成を例に丁寧に解説します。また、非同期処理のメモ化(memoization)戦略と、開発中のプロダクトfile-binへの応用についても紹介します。


なぜ useEffect が必要なのか?

Reactでは、描画(render)とは別に**副作用(side effects)**を扱う必要があります。たとえば、次のようなケース:

  • サーバーからデータを取得
  • ユーザーが入力した値に応じて処理を実行
  • ページロード時に一度だけ何かをしたい

これらは、すべて useEffect の出番です。

基本形

useEffect(() => {
  // 副作用処理
}, []);

第二引数に [](空の配列)を渡すことで、初回マウント時にのみ実行されるという仕様になります。依存配列に変数を入れると、それが変化するたびに再実行されます。


useCallback を使う理由
次に useCallback。これは、関数の「再生成を防ぐ」ために使います。Reactでは、関数を毎回新しく作るのが普通ですが、依存配列に入れたときに予期しない再実行を防ぐために関数の「参照」を保つ必要があります。

以下のように書くと、title, filename, template が変わらない限り、同じ関数インスタンスが使われます:

const handleGenerate = useCallback(async () => {
  const svgMarkup = await createOgpImage(title, filename, template);
  setSvg(svgMarkup);
}, [title, filename, template]);

これにより、useEffecthandleGenerate を依存配列に入れても、意図しない副作用の再実行を防げます。


useEffecthandleGenerate を使う理由

useEffect(() => {
  handleGenerate();
}, [handleGenerate]);

このように、関数を依存配列に入れることはReactのルール上正しい書き方です。handleGenerate の中で title, filename, template を使っている以上、それが変わったら再実行すべきだからです。

では、逆にこれを空の依存配列 [] にするとどうなるでしょうか?

useEffect(() => {
  handleGenerate(); 
}, []); // ← NG!

この場合、titlefilename が変わっても handleGenerate の中の値は更新されないので、古い状態のSVGを生成し続けるバグになります。


非同期処理のメモ化はできるのか?

ここで疑問が出てきます。

SVGを毎回生成するのはコストが高い。
同じパラメータなら、前に生成した結果を使えないのか?

これはいわゆるメモ化(memoization)の話です。

しかし、useMemoは使えない

ReactのuseMemoは値のメモ化には使えますが、非同期処理(Promise)には非対応です。以下のようなコードは動きません

// ❌ useMemo は非同期の値を返せない
const svgMarkup = useMemo(async () => {
  return await createOgpImage(title, filename, template);
}, [title, filename, template]);

解決策:自前でキャッシュを作る

非同期処理をメモ化するには、MapuseRef を使った手動キャッシュ戦略が有効です。

シンプルな例

const cacheRef = useRef<Map<string, string>>(new Map());

useEffect(() => {
  const key = `${title}-${filename}-${template}`;
  const cached = cacheRef.current.get(key);
  if (cached) {
    setSvg(cached);
    return;
  }

  const generate = async () => {
    const svgMarkup = await createOgpImage(title, filename, template);
    cacheRef.current.set(key, svgMarkup);
    setSvg(svgMarkup);
  };
  generate();
}, [title, filename, template]);

このようにすれば、すでに生成されたSVGはキャッシュされ、再生成をスキップできます。


実用例:OGP画像のプレビューコンポーネント

ここまでの技術を使って、以下のような OgpPreview コンポーネントが実装できます。

export function OgpPreview({ title, filename, template = 0 }) {
  const [svg, setSvg] = useState<string | null>(null);

  const handleGenerate = useCallback(async () => {
    const svgMarkup = await createOgpImage(title, filename, template);
    setSvg(svgMarkup);
  }, [title, filename, template]);

  useEffect(() => {
    handleGenerate();
  }, [handleGenerate]);

  return (
    <div className="ogp-preview">
      {svg && <div dangerouslySetInnerHTML={{ __html: svg }} />}
    </div>
  );
}

この設計により、title, filename, template が変わったときだけ新しくSVGが生成され、他のケースでは再利用されるようになります。


file-binにおける応用

この記事で紹介した内容は、筆者が開発中のエンドツーエンド暗号化ファイル共有サービス「file-bin」のOGP生成にも活かされています。

file-binとは?

file-bin は、以下のような特徴を持つモダンなファイル共有サービスです:

  • エンドツーエンド暗号化でセキュリティ重視
  • ゲストアップロード対応(無料で10MBまで)
  • Pro会員は容量制限の拡張、MyPage機能あり
  • OGP画像の自動生成で共有リンクがリッチに見える
  • AWS Amplify + Next.js + Stripe をフル活用

この中でも、共有リンクにOGP画像を生成する機能では、まさにここで解説した非同期処理のメモ化と副作用の最適管理が使われています。

画像生成APIの呼び出し回数が増えると、コストにも影響が出るため、再生成を抑えるキャッシュ戦略は必須でした。


まとめ

  • useEffect は副作用の管理に必須
  • useCallback で関数の再生成を防ぐ
  • useMemo は非同期には使えないので、手動キャッシュを使う
  • SVGや画像の生成などコストが高い処理にはメモ化戦略が効果的
  • 実際のプロダクト(file-bin)でもこの設計が活きている

file-bin を使ってみよう!

あなたも、セキュアかつ手軽にファイルをシェアしたいなら、file-bin をぜひ試してみてください。

  • https://file-bin.com にアクセスして即アップロード
  • 登録すれば Pro 会員として高機能を解放
  • SVGベースのOGP表示で、リンクを送るだけで魅力的なプレビューが表示

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です