2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

入力バリデーションはどこに書く? Facade + Presenter パターンで「責務」を整理する

2
Last updated at Posted at 2026-02-09

本記事の内容は筆者個人の見解であり、所属する組織・団体を代表するものではありません。

はじめに

最近、個人開発の 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 を使うのがベターだと思います。ただ、ここでは階層構造にするメリットを伝えたいのでご容赦ください :pray:

なぜ 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

2
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?