はじめに
前編「【API 型定義 × UI 設計の最適解 #1】Hooks 時代のコンポーネント設計」では、データ取得をコンポーネントに寄せる「Co-location(コロケーション)」戦略について触れました。
本記事では、その設計を TypeScript 環境で実践する際の**「具体的な実装テクニック」**にフォーカスします。
特に、RTK Query で定義された型情報を活かすための方法を中心に解説します。
※ 本記事では例として
openapi-typescriptによる自動生成型を扱いますが、手書きの型定義ファイル (types.ts) を運用している場合でも、本質的なメリット(二重定義の回避)は同じです。
課題:Props 境界で「型」が切れる問題
親コンポーネントでデータを取得し、子コンポーネントへ Props で渡す「従来型」の実装では、以下のような問題が起きがちです。
// API型定義 (自動生成, または共有の型定義ファイル)
type User = { id: string; name: string; age: number; ... };
// 子コンポーネント
// ❌ 課題: ここで改めて型を手動定義する必要がある
type UserNameProps = {
name: string; // ここで API 型との "紐" が切れる!
};
const UserName = ({ name }: UserNameProps) => <p>{name}</p>;
API 側で name の型が変わっても、UserNameProps は追従できず、型の不整合(バグの温床)が発生します。
解決策:コロケーション × 型推論の最大活用
この問題に対する有力なアプローチは、「値を使うコンポーネント自身で API Hooks を呼ぶ(コロケーション)」 ことです。
基本パターン
// UserName.tsx
import { useGetUserQuery } from "./api";
export const UserName = ({ userId }: { userId: string }) => {
// ✅ API 定義から生成された型がそのまま推論される
// user: User | undefined
const { data: user } = useGetUserQuery(userId);
if (!user) return null;
// ここでは user.name は API 定義通りの型であることが保証される
return <p>{user.name}</p>;
};
これにより、手動で Props の型を書く必要がなくなり、API 定義の変更がコンポーネント内部まで自動的に伝播します。
パフォーマンスの最適化:selectFromResult
「末端のコンポーネントごとに Hooks を呼ぶと、無駄な再レンダリングが起きるのでは?」という懸念には、RTK Query の selectFromResult オプションが有効です。
selectFromResult とは
Hooks の戻り値をメモ化し、「本当に必要なデータが変わった時だけ」 コンポーネントを再レンダリングさせる機能です。
export const UserName = ({ userId }: { userId: string }) => {
const { name } = useGetUserQuery(userId, {
// ✅ data 全体が更新されても、name が変わらない限り再レンダリングされない
selectFromResult: ({ data }) => ({
name: data?.name,
}),
});
return <p>{name}</p>;
};
親で巨大なオブジェクトを取得して Props で渡すと、無関係なプロパティ(例: updatedAt)が変わるだけで子コンポーネントも再レンダリングされてしまいますが、この方法ならコンポーネント単位で最適化が可能です。
リクエストの重複について
RTK Query は優秀なキャッシュ機構を持っているため、同じ引数で複数のコンポーネントから同時に Hooks を呼んでも、発行されるネットワークリクエストは 1 回だけです。
そのため、「リクエスト数が増える」という心配は無用です。
「Props を使う場面」の明確化
コロケーションを推奨するとはいえ、Props が完全になくなるわけではありません。以下のようなケースでは Props が適しています。
-
ID や検索条件: データを特定するためのキー(
userIdなど)。 -
UI の状態:
isActive,isOpenなどの制御フラグ。 -
イベントハンドラ:
onClick,onCloseなど。 -
再利用性の高い汎用コンポーネント:
<Card>,<Avatar>のように複数の API で使い回すコンポーネント。特定の Hooks に依存できないため、Props で受け取る。
「API から返ってきた生データ」を Props で渡す頻度を減らす、というのがここでのポイントです。
型安全な実装のための Tips
ローカル変数の型注釈は書かない
VS Code などのエディタは非常に賢いため、Hooks の戻り値から型を推論してくれます。
変に型注釈(const user: User = ...)を書くと、かえって型の範囲を広げてしまったり(any の混入など)、保守コストを増やす原因になります。
基本は推論に任せ、境界(Props や関数の引数)でのみ型を明示するのが、モダンな TS の流儀です。
まとめ
RTK Query × TypeScript 環境で型推論を活かすためのポイントは、コロケーションを採用することです。
データを使う場所で Hooks を呼び出すことで、Props を経由せずに値を取得でき、Hooks の戻り値から型推論がそのまま効きます。Props の型定義が不要になるため、記述量が減り、API の変更にも自動的に追従できます。
コロケーションを採用する場合は、本文で紹介した selectFromResult を活用してパフォーマンスを意識すると良いでしょう。
👉 設計編に戻る: 【API 型定義 × UI 設計の最適解 #1】Hooks 時代のコンポーネント設計
参考リンク・ソース
-
Redux Toolkit docs, "React Hooks - selectFromResult"
selectFromResultを用いて、クエリ結果の一部だけをサブスクライブし、不要な再レンダリングを防ぐ公式の最適化ガイド。
https://redux-toolkit.js.org/rtk-query/usage/queries#selecting-data-from-a-query-result -
Redux Toolkit docs, "Automated Refetching" (Cache Behavior)
同一の引数で複数のコンポーネントからクエリを呼んでも、リクエストは重複せずキャッシュが共有される仕様についての解説。
https://redux-toolkit.js.org/rtk-query/usage/automated-refetching -
TypeScript Handbook, "Type Inference"
型推論は TypeScript の強力な機能であり、明示的な型注釈よりも推論を優先すべき(Best common type)という言語設計の根拠。
https://www.typescriptlang.org/docs/handbook/type-inference.html -
openapi-typescript
OpenAPI から TypeScript 型を自動生成する CLI。この記事で書いている「API 型自動生成」はこれを前提にしている。
https://github.com/drwpow/openapi-typescript