0
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?

Angular async validatorの正しい実装方法|AI実務ノート 編集部

Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

Angular の async validator、一見シンプルに見えて意外とハマるポイントが多いです。私も昔、ユーザー名の重複チェックを実装した際に「なぜか連打されてサーバーが悲鳴を上げる」という事故を起こしました。

この記事では以下を明確にします:

  • debounce や distinctUntilChanged を正しく組み込む実装
  • キャンセル制御と pending 状態の扱い方
  • テスト可能な設計とモック戦略
  • AI を使った問題切り分けの実践例(有料パート)
  • 運用で遭遇する失敗パターンと対策(有料パート)

コピペで動くコード + 設計理由 + テスト戦略をセットで提供するので、明日からのレビューやリファクタに即使えます。

2. 前提(環境・読者層)

  • Angular 14 以降(Standalone API を使わない従来の module ベースでも OK)
  • RxJS 7 以降
  • TypeScript 4.8+
  • 想定読者:Reactive Forms は書けるが、async validator で一度は困った経験がある方

検証環境例:

{
  "@angular/core": "^17.0.0",
  "@angular/forms": "^17.0.0",
  "rxjs": "^7.8.0"
}

3. Before:よくあるつまずきポイント

3-1. 連打問題(debounce なし)

// ❌ ダメな例:入力のたびに API が叩かれる
export function usernameValidator(http: HttpClient): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return http.get(`/api/check-username?name=${control.value}`).pipe(
      map((res: any) => res.exists ? { usernameTaken: true } : null)
    );
  };
}

何が起きるか:

  • ユーザーが「t」「ta」「tar」「taro」と入力すると 4 回 API が飛ぶ
  • サーバー負荷増、レスポンスの順序逆転でバグの温床

3-2. キャンセル漏れ

// ❌ 前のリクエストがキャンセルされない
return http.get(...).pipe(
  delay(1000), // わざと遅延させると順序問題が顕在化
  map(...)
);

古いリクエストが後から返ってきて、最新の入力値と食い違う状態になります。

3-3. pending 状態の誤解

<!--  これだけでは不十分 -->
<div *ngIf="form.get('username')?.pending">チェック中...</div>

初回フォーカス時や debounce 中は pending にならないため、UX が微妙になります。

4. After:基本的な解決パターン

4-1. 正しい async validator の骨格

import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Observable, of, timer } from 'rxjs';
import { map, catchError, switchMap, distinctUntilChanged, debounceTime } from 'rxjs/operators';
 
@Injectable({ providedIn: 'root' })
export class UsernameValidatorService {
  constructor(private http: HttpClient) {}
 
  validate(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      // 空値は同期バリデーターに任せる
      if (!control.value) {
        return of(null);
      }
 
      return timer(500).pipe( // debounce 代わり
        switchMap(() =>
          this.http.get<{ exists: boolean }>(`/api/check-username`, {
            params: { name: control.value }
          })
        ),
        map(res => res.exists ? { usernameTaken: true } : null),
        catchError(() => of(null)) // エラー時は valid 扱い(運用判断)
      );
    };
  }
}

設計ポイント:

  • 「timer(500)」 で debounce 相当を実現(updateOn: 'blur' と組み合わせる手もある)
  • 「switchMap」 で古いリクエストを自動キャンセル
  • 「catchError」 でネットワークエラー時の挙動を明示

4-2. FormControl への適用

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UsernameValidatorService } from './username-validator.service';
 
@Component({
  selector: 'app-signup',
  template: `
    <form [formGroup]="form">
      <input formControlName="username" placeholder="ユーザー名" />
      <div *ngIf="username?.pending" class="spinner">確認中...</div>
      <div *ngIf="username?.hasError('usernameTaken')" class="error">
        このユーザー名は既に使用されています
      </div>
    </form>
  `
})
export class SignupComponent implements OnInit {
  form!: FormGroup;
 
  constructor(
    private fb: FormBuilder,
    private usernameValidator: UsernameValidatorService
  ) {}
 
  ngOnInit() {
    this.form = this.fb.group({
      username: [
        '',
        [Validators.required, Validators.minLength(3)],
        [this.usernameValidator.validate()] // 第三引数に async validator
      ]
    });
  }
 
  get username() {
    return this.form.get('username');
  }
}

4-3. updateOn: 'blur' の活用

this.form = this.fb.group({
  username: [
    '',
    [Validators.required],
    [this.usernameValidator.validate()]
  ]
}, { updateOn: 'blur' }); // フォーカスアウト時だけ検証

入力中に API を叩きたくない場合はこれが最速解。ただし UX とのトレードオフです。

0
0
0

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
0
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?