React経験者がAngularでつまずく5点とClaude Codeへの頼み方
React出身者がAngularで戸惑うsignals・standalone・DI・RxJSを整理し、Claude Codeに型安全なフォームとテストを書かせる具体的な指示まで実例で紹介。
「Angularなんて、Reactと同じフロントエンドでしょ」
そう思って案件に入った僕は、最初の30分でフリーズしました。useState がない。コンポーネントを並べただけでは画面に出ない。@Injectable ってなんだ。フォームひとつ作るのに FormGroup と Validators を import して……Reactなら3行で終わる入力欄が、Angularでは作法を知らないと一歩も進めない。
そこで Claude Code に「Angularでフォーム作って」と丸投げしたら、見た目は動きそうな差分が返ってきました。でも開けてみると、テストなし、API型なし、送信失敗時のUIなし。しかも一部が古い NgModule 前提で、今のAngularでは余計なコードでした。
問題はClaude Codeの賢さじゃなくて、僕がAngularの「考え方」を渡せていなかったことです。Reactの頭のままだと、的を射た指示が書けない。この記事では、React経験者が必ず引っかかる5点を整理して、そのうえでClaude Codeに「レビューに耐える差分」を書かせるコツを、問い合わせ登録フォームを題材に紹介します。
この記事の要点
- AngularはReactと設計思想が逆。「ライブラリの寄せ集め」ではなく「全部入りフレームワーク」で、DI・RxJS・フォームが標準。React経験者はここで戸惑う。
- 最新のリアクティビティは signals(
signal/computed/effect)。useState/useMemo/useEffectに近いが、依存配列がいらない。公式で安定版。 - Angular v19以降、コンポーネントは standalone がデフォルト。
standalone: trueはもう書かない。古い記事のコードを真似ると逆に冗長になる。 - Claude Codeへの指示は「フォーム作って」では弱い。対象ファイル・禁止事項(
ngModel禁止、any禁止)・検証コマンド・レビュー観点まで渡すと差分の質が一気に上がる。 - 最後に載せた
signalの最小コードはコピペで動く。まずこれで「Reactとどう違うか」を体で覚えるのが近道。
React経験者が引っかかる5点
先に地図を渡します。Reactを書ける人がAngularで「えっ」となるのは、だいたいこの5箇所です。Claude Codeに指示を出すときも、この差を意識すると言葉が具体的になります。
| つまずく点 | Reactでは | Angularでは |
|---|---|---|
| 状態管理 | useState / useReducer | signals(signal / computed / effect) |
| コンポーネント登録 | import して JSX に置くだけ | imports: 配列に書く(v19以降は standalone がデフォルト) |
| 依存の受け渡し | props か Context | DI(inject() でサービスを注入) |
| 非同期データ | Promise / async-await が中心 | RxJS(Observable の流れで扱う) |
| フォーム | 自作 or react-hook-form | Reactive Forms(標準でフレームワークに入っている) |
ざっくり言うと、Reactは「必要なものを自分で選んで足す」文化、Angularは「最初から全部入っている」文化です。どちらが偉いという話ではなく、思想が違う。この違いを飲み込むと、Angularの「冗長に見える書き方」が、実はチーム開発で迷わないための仕組みだと分かってきます。
ここからは、特に効く3つ(signals・standalone・DI/RxJS)を順に見ていきます。
signals:useStateに似ているけど依存配列がいらない
Angularの最新のリアクティビティが signals です。値の入れ物で、中身が変わると、それを使っている場所が自動で更新されます。Reactのフックを知っているなら、対応はこう覚えると速いです。
signal(0)≒useState(0)。ただし更新はcount.set(1)かcount.update(v => v + 1)。computed(() => ...)≒useMemo。依存配列はいらない。中で読んだsignalを自動で追跡する。effect(() => ...)≒useEffect。これも依存配列なし。
useEffect の第二引数を書き忘れて無限ループ、というReactあるあるが、Angularのsignalsでは構造的に起きません。ここは素直に「楽になった」と感じる部分です。
公式(Angular signals ガイド)で安定版として案内されている、最小の例がこれです。コンポーネントもサービスもいらない、純粋なsignalの動きだけを確認できます。
import { signal, computed, effect } from '@angular/core';
// 書き換えできる値。useState(0) のイメージ
const count = signal(0);
// count から自動で導出される値。依存配列を書かなくていい
const doubled = computed(() => count() * 2);
// count か doubled が変わるたびに走る。useEffect に近い
effect(() => {
console.log(`count=${count()} / doubled=${doubled()}`);
});
count.set(3); // → effect が走り "count=3 / doubled=6"
count.update((v) => v + 1); // → "count=4 / doubled=8"
値を読むときは count() と関数呼び出しになるのがReactとの見た目の違いです。最初は違和感がありますが、「読むときはカッコをつける」とだけ覚えれば慣れます。Claude Codeにフォームの状態を持たせるときも、signal と computed を使うよう明記しておくと、後述のフォームコードのように送信中フラグや成功表示がきれいにまとまります。
standalone:v19以降は「書かない」が正解
ここはReact経験者というより、古いAngular記事を読んだ人が一番ハマるところです。
少し前のAngularでは、コンポーネントを使うのに NgModule という「登録簿」が必須でした。それを不要にしたのが standalone component です。そして Angular公式ブログ(The future is standalone!) のとおり、v19からは standalone がデフォルトになりました。
何が変わったかと言うと——
@Componentにstandalone: trueを書く必要がなくなった(書いても害はないが冗長)。- 逆に、古い
NgModule方式を使いたいコンポーネントだけstandalone: falseを明記する。 ng updateの自動移行が、既存のstandalone: trueを消し、NgModule配下にはstandalone: falseを付けてくれる。
つまり、ネット上に転がっている「standalone: true を付けましょう」という解説は、今書くと古いコードになります。Claude Codeも学習データの都合で standalone: true を付けたがることがあるので、指示で「v19前提。standalone: true は書かない」と一言添えるのが地味に効きます。
コンポーネントが使う部品(ReactiveFormsModule など)は imports: 配列に並べます。Reactで言えば「使うものを import して JSX に置く」のと同じ発想を、配列で宣言する形です。
DIとRxJS:propsの代わりに「注入」、Promiseの代わりに「流れ」
残る2つ、DI(依存性の注入)とRxJSも、React出身者が身構えるポイントです。
DI は、サービス(ロジックの置き場)をコンポーネントに「注入」する仕組みです。Reactでロジックを共有するならカスタムフックかContextですが、Angularは inject() 一発で済みます。
private readonly ticketService = inject(TicketService);
これでサービスのインスタンスが手に入ります。new しないのがポイントで、テストのときに偽物(モック)と差し替えやすくなる。後でテストコードを載せますが、この差し替えのしやすさがDIの最大のうまみです。
RxJS は、非同期データを「一回限りの結果(Promise)」ではなく「流れてくるデータ(Observable)」として扱う考え方です。HttpClientの戻り値も Observable です。最初は身構えますが、HTTP1回きりの用途なら、subscribe で受け取って finalize で後始末する、くらいの理解で十分実務に入れます。
身近な例えだと、Promiseは「自販機(押したら1本出る)」、Observableは「水道(蛇口をひねると流れ続け、自分で止める)」です。HTTPは1本でいいので自販機的に使い、止め忘れ(メモリリーク)にだけ気をつける、という温度感です。
題材:問い合わせ登録フォームをClaude Codeに作らせる
ここから実装です。サービス層(API型)を先に固め、その後フォーム、最後にテストの順で進めます。UIは変わりやすいですが、API契約はチームで合意するものなので、型から固めるとレビューが楽になります。
まず src/app/data/ticket.service.ts。Observable を返し、HttpClientの型引数でレスポンスの形を明示します。
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export type TicketPriority = 'low' | 'medium' | 'high';
export interface TicketDraft {
title: string;
email: string;
priority: TicketPriority;
message: string;
}
export interface Ticket extends TicketDraft {
id: string;
createdAt: string;
}
@Injectable({ providedIn: 'root' })
export class TicketService {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/tickets';
listTickets(): Observable<Ticket[]> {
return this.http.get<Ticket[]>(this.baseUrl);
}
createTicket(draft: TicketDraft): Observable<Ticket> {
return this.http.post<Ticket>(this.baseUrl, draft);
}
}
standalone構成では、HttpClient を app.config.ts で provideHttpClient() を使って有効化します。古い HttpClientModule を imports に足すのは今は非推奨なので、Claude Codeにも明記します。
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient()],
};
ひとつ落とし穴を共有します。サービスに「送信に失敗しました」というUI用の日本語を混ぜると、多言語化や画面ごとの出し分けで詰みます。サービスはAPI通信と型に集中させ、表示はコンポーネント側で持つ。これはReactでカスタムフックとUIを分けるのと同じ感覚です。
フォーム本体:signalsとReactive Formsを合わせる
次にフォームコンポーネントです。signalsで送信中フラグと成功表示を持ち、入力と検証はReactive Formsで管理します。v19デフォルトに合わせ、standalone: true は書きません。
import { Component, computed, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
import { finalize } from 'rxjs';
import { Ticket, TicketService, TicketPriority } from '../../../data/ticket.service';
@Component({
selector: 'app-ticket-intake',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()" aria-label="問い合わせ登録フォーム">
<label>タイトル<input type="text" formControlName="title" /></label>
<label>メール<input type="email" formControlName="email" /></label>
<label>
優先度
<select formControlName="priority">
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
</select>
</label>
<label>本文<textarea formControlName="message"></textarea></label>
@if (form.hasError('submit')) {
<p role="alert">登録に失敗しました。もう一度お試しください。</p>
}
@if (savedTicket(); as ticket) {
<p role="status">チケット {{ ticket.id }} を登録しました</p>
}
<button type="submit" [disabled]="isSubmitDisabled()">
{{ saving() ? '送信中...' : '登録する' }}
</button>
</form>
`,
})
export class TicketIntakeComponent {
private readonly ticketService = inject(TicketService);
// signals で状態を持つ(useState 相当)
readonly saving = signal(false);
readonly savedTicket = signal<Ticket | null>(null);
readonly form = new FormGroup({
title: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(5)],
}),
email: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.email],
}),
priority: new FormControl<TicketPriority>('medium', { nonNullable: true }),
message: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(20)],
}),
});
// computed で送信可否を導出(useMemo 相当、依存配列なし)
readonly isSubmitDisabled = computed(() => this.form.invalid || this.saving());
submit(): void {
this.form.markAllAsTouched();
this.form.setErrors(null);
if (this.form.invalid) return;
this.saving.set(true);
this.ticketService
.createTicket(this.form.getRawValue())
.pipe(finalize(() => this.saving.set(false))) // 成否に関わらず送信中を解除
.subscribe({
next: (ticket) => {
this.savedTicket.set(ticket);
this.form.reset({ title: '', email: '', priority: 'medium', message: '' });
},
error: () => this.form.setErrors({ submit: true }),
});
}
}
このフォームには、僕が実際にやらかした落とし穴が3つ詰まっています。1つ目は FormsModule と ReactiveFormsModule を混ぜて、同じ入力に ngModel と formControlName を両方書くこと。これは静かに壊れます。2つ目は送信中の二重クリック対策忘れ。isSubmitDisabled で塞いでいます。3つ目は form.value をそのままAPIに送ること。nullableが混ざるので、nonNullable と getRawValue() で型を確定させます。
Claude Codeへの依頼は、こう具体的に書きます。
src/app/features/tickets/ticket-intake に standalone component を実装してください。
前提: Angular v19。standalone がデフォルトなので standalone: true は書かないでください。
Reactive Forms を使い、ngModel は使わないでください。
送信中はボタンを disabled にし、API 失敗時は role="alert" で表示してください。
状態は signal / computed で持ってください。
TicketService の型を使い、any は追加しないでください。
実装後に関連する unit test も追加してください。
テスト:HttpClientとサービスのモックで失敗系まで
DIのうまみが効くのがテストです。HttpTestingController で実ネットワークに出さずにHTTPを検証し、コンポーネント側はサービスをモックに差し替えます。新しいCLIプロジェクトのテストガイドはVitest前提ですが、既存プロジェクトはKarma/Jasmineのこともあるので、Claude Codeには必ず現在のテストランナーを確認させます。
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { TicketService } from './ticket.service';
describe('TicketService', () => {
let service: TicketService;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [TicketService, provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(TicketService);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('APIにPOSTしてチケットを作る', () => {
const draft = {
title: '請求エクスポートが止まる',
email: '[email protected]',
priority: 'high' as const,
message: '月次の請求エクスポートが2時間終わりません。',
};
service.createTicket(draft).subscribe((ticket) => {
expect(ticket.id).toBe('T-100');
expect(ticket.priority).toBe('high');
});
const req = http.expectOne('/api/tickets');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(draft);
req.flush({ ...draft, id: 'T-100', createdAt: '2026-06-07T09:00:00.000Z' });
});
});
コンポーネントテストでは、DOMの文言だけを見るテストに偏ると壊れやすいので、入力 → 送信 → サービス呼び出し → 成功表示を一連で確認します。TicketService をモックに差し替えられるのは、inject で依存を受けているからです。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { vi } from 'vitest';
import { TicketIntakeComponent } from './ticket-intake.component';
import { TicketService } from '../../../data/ticket.service';
describe('TicketIntakeComponent', () => {
let fixture: ComponentFixture<TicketIntakeComponent>;
const createTicket = vi.fn();
beforeEach(async () => {
createTicket.mockReturnValue(
of({
id: 'T-101',
title: '請求エクスポートが止まる',
email: '[email protected]',
priority: 'high',
message: '月次の請求エクスポートが2時間終わりません。',
createdAt: '2026-06-07T09:00:00.000Z',
}),
);
await TestBed.configureTestingModule({
imports: [TicketIntakeComponent],
providers: [{ provide: TicketService, useValue: { createTicket } }],
}).compileComponents();
fixture = TestBed.createComponent(TicketIntakeComponent);
fixture.detectChanges();
});
it('正しい入力で送信できる', () => {
const component = fixture.componentInstance;
component.form.setValue({
title: '請求エクスポートが止まる',
email: '[email protected]',
priority: 'high',
message: '月次の請求エクスポートが2時間終わりません。',
});
component.submit();
expect(createTicket).toHaveBeenCalledWith(component.form.getRawValue());
expect(component.savedTicket()?.id).toBe('T-101');
});
});
ブラウザ操作まで見たいなら、PlaywrightをCIに入れるのが現実的です。page.route でAPIを差し替え、ユーザー操作が通るかを確認します。これらの観点はClaude Codeのテスト戦略で扱った「失敗系を必ず1本入れる」考え方とそのまま地続きです。
レビュー指示まで含めて初めて「公開できる差分」
実装させたら、Claude Code自身にレビューさせます。「良さそう?」と聞くと抽象的な返事しか来ないので、観点を固定するのがコツです。これはCLAUDE.mdベストプラクティスで書いた「曖昧な指示を作業契約に変える」やり方の応用です。
今回のAngular差分を批判的にレビューしてください。
観点:
1. v19前提で standalone: true を不要に書いていないか / imports は正しいか
2. Reactive Forms で nullable 値や二重送信の問題がないか
3. TicketService が UI 文言などの表示責務を持っていないか
4. HttpClient テストが実ネットワークに出ていないか
5. unit test に失敗系(API エラー時の role="alert" 表示)があるか
6. any、不要な subscribe、メモリリーク、アクセシビリティの問題がないか
ファイル名と行番号つきで、公開前に直すべき順に指摘してください。
複数人で同時に触っているときは、指示に「このfeature配下だけ」「routingとproviderは触らない」「未コミット変更は戻さない」と添えます。これはAIへの気遣いではなく、チーム開発の事故を防ぐための作業契約です。設計全体を任せる枠組みはハーネスエンジニアリング入門が詳しいです。
よくある質問
Q. React経験があれば、Angularは何日で書けるようになりますか? 画面1枚作るだけなら1〜2日です。詰まるのは構文ではなく思想なので、この記事の5点(signals・standalone・DI・RxJS・Reactive Forms)の対応表を手元に置いて、まず小さなフォームを1つClaude Codeと作るのが最短です。
Q. signalsとRxJSは、どちらを使えばいいですか? 画面の状態(送信中、選択中など)はsignals、HTTPなどの非同期イベントはRxJS、と分けるのが今の素直な形です。HTTPの結果を最終的にsignalへ落とす、という組み合わせがよく使われます。
Q. 古いコードで見かける standalone: true は消すべきですか?
v19以降はデフォルトなので消してかまいません。ng update の自動移行が消してくれます。手で書く新規コードでは最初から書かないのが正解です。
Q. Claude Codeは古いNgModule前提のコードを出してきませんか? 出すことがあります。指示の冒頭に「Angular v19前提。standaloneがデフォルト。NgModuleは使わない」と書くだけでかなり減ります。出てきたら、上のレビュー指示の観点1で弾けます。
Q. provideHttpClient と HttpClientModule の違いは?
役割は同じ「HttpClientを使えるようにする」ですが、standalone時代の標準は provideHttpClientです。HttpClientModuleは古い書き方なので、新規コードでは使いません。
まとめ:思想を渡せば、Claude Codeは速い
最初に「フォーム作って」と頼んで返ってきた差分は、ngModel混在・API型なし・失敗系テストなしで、しかも一部が古いNgModule前提でした。原因はClaude Codeではなく、僕がAngularの考え方を言葉にできていなかったことです。
そのあと、Angular v19前提・provideHttpClient・signals・Reactive Forms・HttpTestingControllerまで指定したら、手直しは文言とCSSにほぼ絞れました。特に効いた指示は「standalone: trueは書かない」「anyを追加しない」「サービスにUI文言を入れない」「unit testに失敗系を入れる」の4つです。
React出身者の強みは、状態・コンポーネント・テストという骨格をもう知っていることです。あとはAngular流の言い回し(signals・standalone・DI・RxJS)に翻訳するだけ。その翻訳さえClaude Codeに渡せれば、Angularは驚くほど速く書けます。まずは上の signal の最小コードを動かして、「読むときはカッコ」の感覚を1分で掴んでみてください。
チーム導入や設計レビューまで踏み込みたい人は研修・導入相談も覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。