本記事の内容は筆者個人の見解であり、所属する組織・団体を代表するものではありません。
はじめに
最近、個人開発の Todo アプリでアーキテクチャを設計していて、ふと立ち止まりました。
「フォームの入力チェック(バリデーション)、どこに書くのが正解なんだ?」
View (JSX) に書くと汚くなる。
かといって API 通信を担当する Facade (Custom Hook) に書くと、Facade が「UI の泥臭い状態」を持ってしまう。
今回は、この 「入力バリデーション」 をテーマに、Facade と Presenter の役割分担 について、自分なりの結論(Facade + Presenter パターン)をまとめます。
TL;DR(結論)
-
Presenter の仕事:
- 「空文字はダメ」「10文字以内」といった UI バリデーション を担当する
- 不正なデータはここで弾き、Facade には渡さない
-
Facade の仕事:
- Server State (TanStack Query) を管理する
- 「正しいデータが渡される前提」で、サーバーとの同期(保存・キャッシュ更新)に専念する
フォームのバリデーションは React Hook Form を使うのがベターだと思います。ただ、ここでは階層構造にするメリットを伝えたいのでご容赦ください ![]()
なぜ Facade だけじゃダメなのか?
よくある「ちょっと太った Custom Hook」の例を見てみましょう。
Facade(データ管理役)が、バリデーションまでやってしまっているパターンです。
// ❌ Facade が「UI の都合」を知りすぎている
export const useTodoFacade = () => {
const [error, setError] = useState(null); // UI のエラー状態
const mutation = useMutation(...);
const addTodo = (text: string) => {
// 😱 Facade の中でバリデーションしちゃってる
if (!text) {
setError('入力してください');
return;
}
mutation.mutate(text);
};
return { addTodo, error };
};
Facade は本来、API 通信・キャッシュ更新・リトライなど「サブシステムの複雑さ」を隠す入口です。
ここに「未入力です」みたいな UI バリデーションエラーまで載せ始めると、mutation.error が表す サーバー/ネットワークの失敗と同列に扱うことになり、表示や復帰(いつ消す?リトライ対象?)のルールが曖昧になります。
だから 即時フィードバック(Client) は Presenter に寄せて、Facade は Server State(TanStack Query が持つ失敗/ローディング/キャッシュ)に集中させるのが自然です。
解決策:Presenter を「防波堤」にする
そこで、Presenter の登場です。
Presenter は View と Facade の間に立ち、「汚い入力(User Input)」を「綺麗なデータ(Domain Data)」に変換する 役割を担います。
1. Facade
Facade はバリデーションをしません。「データさえ渡せば保存してくれる」という サーバーサイドの状態管理 に徹します。
// ✅ Facade: Server State Management
// UI のことは知らない。
export const useTodoFacade = () => {
const mutation = useMutation({
mutationFn: (title: string) => api.post('/todos', { title }),
onSuccess: () => queryClient.invalidateQueries(['todos']),
});
return {
addTodo: mutation.mutateAsync, // 関数をそのまま貸し出すだけ
isSaving: mutation.isPending,
};
};
2. Presenter
ここでバリデーションを行います。
「UI から来たイベント」を受け止め、チェックに合格したときだけ Facade を呼びます。
// ✅ Presenter: Client State Management & Validation
// UI の泥臭い処理(バリデーション)を引き受ける門番。
export const useTodoPresenter = () => {
const { addTodo } = useTodoFacade(); // 機能借りてくる
const [validationError, setValidationError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent, text: string) => {
e.preventDefault();
// 🛑 ここで止める(防波堤)
if (!text.trim()) {
setValidationError('文字を入力してください');
return; // Facade には指一本触れさせない
}
// ✅ 通過したらエラーを消して、Facade を呼ぶ
setValidationError(null);
await addTodo(text);
};
return {
handleSubmit,
validationError, // View にはこれを見せる
};
};
この構成のメリット
1. View が圧倒的にシンプルになる
View は Presenter から渡された validationError を表示するだけ。条件分岐ロジックが JSX から消え去ります。
2. Facade が「信頼できるデータ」だけを扱える
Facade の addTodo が呼ばれた時点で、それは「空文字ではない」ことが保証されています。
これにより、API クライアントの実装がシンプルになり、Server State(ローディング、サーバーエラー、キャッシュ) の管理だけに集中できます。
3. テストが書きやすい
- バリデーションのテスト:Presenter だけをテストすればOK(API モック不要)。
- データ保存のテスト:Facade だけをテストすればOK(バリデーション無視でデータ投げられる)。
まとめ
「Facade を Presenter でラップする」 という行為は、単なるパススルーではありません。
それは 「入力チェック(Client State)」と「データ保存(Server State)」の責任分界点を明確にする という、堅牢なアプリケーションを作るための重要なテクニックです。
「バリデーション、どこに書こう?」と迷ったら、「Presenter を立てて、そこで弾く」 と覚えておくと、設計がスッキリするかもしれません。
コラム:React の "Facade" は「リポジトリパターン」なのか?
「これ、リポジトリじゃないの?」という違和感
Facade + Presenter パターンを実装していると、ふとこんな疑問が湧いてきませんか?
「
useTodoFacadeって、getAllしたりsaveしたりしてるじゃん。これ、DDD で言うところの 『リポジトリ(Repository)』 じゃないの? なんで Facade って呼んでるの?」
この違和感、大正解 です。
実際、役割の 8割 はリポジトリと同じです。データソース(REST API, GraphQL, Firebase)を隠蔽し、ドメインデータを返してくれます。
しかし、残りの 2割 に 決定的な違い があります。
それが 「時間の概念(State)」 です。
バックエンドのリポジトリ vs フロントエンドの Facade
1. 純粋なリポジトリ(Backend / Stateless)
バックエンドのリポジトリは、基本的に ステートレス です。
「データベースから取ってきて」と頼めば、その瞬間のデータを Promise で返して終わりです。
// Backend Repository
class TodoRepository {
async getAll(): Promise<Todo[]> {
return db.select().from(todos); // 1回返して終わり
}
}
2. React の Facade(Frontend / Stateful)
一方、今回作った useTodoFacade はどうでしょう?
TanStack Query を使っているため、ただデータを返すだけでなく、「データを保持(キャッシュ)し、変更を監視し続ける」 という役割を持っています。
// Frontend Facade (Hook)
const useTodoFacade = () => {
const { data } = useSuspenseQuery(...);
// 👆 Promise ではなく、"現在のデータ(State)" を返している!
return { todos: data };
};
これが決定的な違いです。
Facade はリポジトリ(Todo.api.ts)を内包していますが、それ自体は 「生きたデータストア(Reactive Store)」 として振る舞っています。
正体は「リポジトリへの常時接続(Subscription)」
もし useTodoFacade をより正確な名前にリネームするとしたら、useTodoRepositorySubscription あるいは useTodoStore が近いでしょう。
- リポジトリ(Repo):蛇口。ひねれば水(データ)が出る
- ファサード(Facade):バケツ。蛇口から出た水を溜めておき、View がいつでも汲めるようにしている
フロントエンド(React)は「画面を描画し続ける」必要があるため、ただの蛇口(関数)では足りません。
必ずどこかで水を溜めておくバケツ(State)が必要になります。
結論:名前は何でもいい、役割が大事
今回のアーキテクチャでは、あえて Facade という言葉を使っています。
それは「複雑な裏側(API通信、キャッシュ管理、楽観的更新)を隠して、シンプルな窓口を提供する」というデザインパターンの意図を重視したからです。
-
Todo.api.ts= 純粋な Repository(ステートレスな通信) -
Todo.facade.ts= Repository + Store(ステートフルなデータ管理)
「Facade の中にはリポジトリが入ってるんだな」というメンタルモデルで捉えておくと、スッキリするかもしれません。
参考 URL
https://refactoring.guru/design-patterns/facade
https://www.cs.unc.edu/~stotts/GOF/hires/pat4efs.htm