Angular開発メモ

調べたことを記録しておきます。

同じAPIリクエストがほぼ同時に複数回送信される問題

Webアプリで、同じ更新リクエストがほぼ同時に複数回送信されることがある。

イメージとしてはこんな感じだ。

リクエストA → DB更新成功
リクエストB → エラー
リクエストC → エラー

最初のリクエストでDBの更新自体は成功している。

そのためユーザーから見ると処理は正常に完了しており、大きな実害は発生していない。

しかし、本来1回しか送信されないはずのリクエストが複数回送信されている以上、何らかの問題が潜んでいる。

しかも厄介なのは、毎回ではなく「たまに」発生することだ。

今回は、このような問題について原因として何が考えられるのか、そしてどのような順番で調査するのがよいのかを整理してみる。

まず疑うのはフロント側

今回のような

  • 同じAPIがほぼ同時に呼ばれる
  • 1回目は成功する
  • 2回目以降はエラーになる
  • 毎回ではなく、ときどき発生する

という現象では、まずフロント側の多重実行を疑いたい。

Webアプリでは、同じ処理が意図せず複数回実行されることはそれほど珍しくない。

特にAngularのようにイベントと非同期処理を多用するフレームワークでは、複数の処理経路から同じAPIに到達してしまう可能性がある。

よくある原因

ボタンの連打

最も単純なのがこれ。

<button (click)="save()">保存</button>

API通信中もボタンが有効であれば、ユーザーが短時間に複数回クリックすることで複数リクエストが発生する。

通信開始時にボタンを無効化するなどの対策が考えられる。

clickとsubmitの両方から呼んでいる

フォームでは、イベントの組み合わせによって同じ処理が複数経路から呼ばれることがある。

たとえば、

<form (ngSubmit)="save()">
  <button type="submit" (click)="save()">保存</button>
</form>

のような実装では、クリックとsubmitの両方からsave()が実行される。

これは比較的分かりやすい例だが、実際のアプリでは親子コンポーネントや共通処理を経由するため、もっと見つけにくい形になっていることもある。

Observableの多重subscribe

AngularではRxJSを利用するため、Observable周辺も確認したい。

同じObservableを複数箇所でsubscribe()していたり、コンポーネントが生成されるたびにsubscriptionが追加されていたりすると、1つのイベントから複数回処理が実行される可能性がある。

特に、

valueChanges
router.events
Subject
EventEmitter

などを起点としてAPIを実行している場合は注意したい。

複数のイベントから同じ処理へ到達している

個人的には今回のような「たまに発生する」問題では、ここを特に疑いたい。

たとえば、

ユーザー操作
 ├─ イベントA ─┐
 │             ├─ update()
 └─ イベントB ─┘

という構造になっている場合だ。

通常はイベントAだけが発生するが、特定のタイミングではイベントBも発生する。

すると、ほぼ同時に同じAPIが呼ばれる。

単純な実装ミスなら毎回発生する可能性が高い。

「たまにしか発生しない」ということは、タイミングや状態によって処理経路が重なるタイプの問題である可能性がある。

retryやバックエンド側の再実行

もちろんフロント以外の可能性もある。

  • HTTP通信のretry
  • タイムアウト後の再送
  • バックエンド内部での多重実行
  • プロキシやインフラによる再試行

なども候補になる。

そのため最初から「フロントのバグだ」と決めつけるのではなく、まず「どこでリクエストが増えたのか」を特定する必要がある。

調査では最初にフロントかバックかを切り分ける

重要なのは、いきなりコードを細かく読むことではない。

まず、

本当にフロントから複数リクエストが送信されているのか

を確認する。

ブラウザのDeveloper ToolsのNetworkタブで再現できれば分かりやすい。

ただし「たまにしか発生しない」問題では、開発環境で簡単に再現できない可能性も高い。

そこでログを利用する。

リクエストIDを付ける

調査をしやすくする方法として、フロントでリクエストごとにIDを発行する方法がある。

const requestId = crypto.randomUUID();

console.log('update start', requestId);

this.service.update(data).subscribe({
  next: () => console.log('update success', requestId),
  error: error => console.log('update error', requestId, error),
});

さらに、このIDをバックエンドにも送る。

するとログ上で、

12:00:00.100 requestId=A
12:00:00.112 requestId=B
12:00:00.118 requestId=C

のように確認できる。

A、B、Cという異なるIDがフロントで生成されていれば、

フロント側でAPI呼び出し処理そのものが3回実行された

可能性が高い。

逆にフロントでは1回しか記録されていないのに、バックエンドでは複数回処理されているのであれば、調査対象を通信経路やバックエンド側へ移せる。

まずこの境界を明確にすることが重要だ。

APIを呼んでいる場所から逆方向に追う

フロント側が怪しいと分かったら、APIを呼び出している箇所を検索する。

たとえば、

this.xxxService.update(...)

を検索する。

ただし、それだけでは不十分なこともある。

重要なのは、

HTTP API
↑
Service
↑
update()
↑
イベント処理
↑
ユーザー操作

というように、APIから逆方向に処理経路を追うことだ。

そして、

「このAPIへ到達できる経路はいくつあるのか」

を確認する。

1本だと思っていた経路が、実は2本、3本存在していることがある。

今回ならこの順番で調査する

今回のような現象なら、私は次の順番で確認する。

  1. Networkやログで、本当に同一APIが複数回呼ばれていることを確認
  2. フロントでAPI呼び出し直前にログを出す
  3. リクエストIDを付けてバックエンドログと突き合わせる
  4. API呼び出し箇所を検索する
  5. そこへ到達するイベントを逆方向に追う
  6. click、submit、親子イベントなどの二重発火を確認する
  7. ObservableのsubscribeやRxJSの処理を確認する
  8. コンポーネントのライフサイクルを確認する
  9. retry設定を確認する
  10. フロントで説明できなければバックエンドやインフラを調べる

闇雲にコードを読むより、

どのレイヤーで処理が複数になったのか

を先に切り分けた方が調査範囲を大幅に狭められる。

フロントを直すだけで終わらせない

仮に原因がフロントの二重送信だったとしても、もう一つ考えておきたいことがある。

それがAPIの「冪等性」だ。

冪等性とは簡単に言えば、

同じ操作が複数回来ても、システムの状態がおかしくならない性質

のことである。

Webでは、二重クリックだけでなく、通信障害やretryなどによって同じリクエストが複数回届く可能性がある。

そのため重要な更新処理では、

同じリクエストが2回来た
↓
1回目は処理
↓
2回目は「すでに処理済み」と判断
↓
安全に終了

という設計も検討する価値がある。

つまり、

フロントでは多重送信を防ぐ。

そして、

バックエンドでは多重送信されても壊れないようにする。

この二段構えが理想になる。

まとめ

同じAPIリクエストがほぼ同時に複数回送信される問題は、Webアプリではそれほど珍しいものではない。

特に「毎回ではなく、たまに発生する」という場合は、複数イベントや非同期処理が特定のタイミングで重なっている可能性を疑いたい。

ただし、最初からフロントのバグだと決めつけるべきではない。

まず確認すべきなのは、

「どのレイヤーで1回が複数回になったのか」

である。

フロント、通信経路、バックエンド、DBと順番に境界を切り分けていけば、調査範囲はかなり小さくできる。

そして原因を修正した後も、バックエンド側で冪等性を確保できないか検討する。

「1回しか来ないはず」と考えるのではなく、

「複数回来ることはあり得る。それでも安全に処理できる」

という設計にしておくことが、Webアプリを堅牢にする上では重要なのだと思う。

【Angular】ページ遷移してもスクロール位置が変わらない理由と解決方法

Angularで画面遷移を実装していると、

「別のページへ遷移したのに、前のページのスクロール位置がそのまま残っている」

という現象に遭遇することがあります。

これはAngularの仕様であり、withInMemoryScrolling を利用することで簡単に改善できます。

原因

AngularはSPA(Single Page Application)です。

通常のWebサイトでは、

  • 新しいページへ移動 → 一番上から表示
  • 戻るボタン → 前回のスクロール位置を復元

というブラウザの標準動作になります。

しかしAngularでは画面を書き換えているだけなので、ブラウザから見ると同じページです。

そのため、何もしないとスクロール位置はそのままになります。

例えば

/home (500pxまでスクロール)

↓

/about

と遷移しても、/about が500px付近から表示されてしまいます。

解決方法

app.config.ts の provideRouter() に withInMemoryScrolling() を追加します。

import { provideRouter, withInMemoryScrolling } from '@angular/router';

provideRouter(
  routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'top',
  })
);

これだけで、ページ遷移時に常にページ先頭から表示されるようになります。

anchorScrollingとは?

withInMemoryScrolling にはアンカーリンク用の機能も用意されています。

provideRouter(
  routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'top',
    anchorScrolling: 'enabled',
  })
);

anchorScrolling を有効にすると、

/#section1

のようなURLへアクセスした際、

<div id="section1">

まで自動でスクロールしてくれます。

固定ヘッダーがある場合

固定ヘッダーを採用しているサイトでは、アンカーリンクで移動すると見出しがヘッダーの裏に隠れてしまうことがあります。

そのような場合は、スクロール位置をJavaScriptで計算して制御する方法もあります。

const element = document.getElementById(id);

const headerOffset = 56;

const y =
  element.getBoundingClientRect().top +
  window.scrollY -
  headerOffset;

window.scrollTo({
  top: y,
  behavior: 'smooth',
});

この方法なら、ヘッダーの高さに応じて自由にスクロール位置を調整できます。

まとめ

  • AngularはSPAのため、ページ遷移時にスクロール位置は自動でリセットされない。
  • withInMemoryScrolling() を利用するとスクロール位置を管理できる。
  • scrollPositionRestoration: 'top' を指定するとページ先頭から表示できる。
  • アンカーリンクは anchorScrolling が利用できる。
  • 固定ヘッダーがある場合は、自前でスクロール位置を計算すると柔軟に制御できる。

【Angular】複数の子コンポーネントをまたぐフォームはどう管理する?

Angularでフォームを作っていると、こんな構成にしたくなることがあります。

  • 親コンポーネント
    • 名前入力コンポーネント
    • 住所入力コンポーネント
    • 確認ボタンコンポーネント

つまり、

「1つのフォームを、複数の子コンポーネントで分割して使いたい」

というケースです。

このとき、

  • 親から子へフォームを渡す?
  • フォーム専用サービスを作る?

どちらがよいのでしょうか。

今回はAngularでよくあるフォーム管理パターンを整理します。


結論:まずは「親がFormGroupを持つ」が基本

Angularでは、まずはこれが定番です。

親コンポーネント

import { Component } from '@angular/core';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
} from '@angular/forms';

@Component({
  selector: 'app-parent',
  imports: [ReactiveFormsModule],
  templateUrl: './parent.component.html',
})
export class ParentComponent {
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
    address: new FormControl(''),
  });
}

子コンポーネントへフォームを渡す

親HTML

<app-name-form [form]="form"></app-name-form>

<app-address-form [form]="form"></app-address-form>

子コンポーネント側

TypeScript

import { Component, Input } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-name-form',
  imports: [ReactiveFormsModule],
  templateUrl: './name-form.component.html',
})
export class NameFormComponent {
  @Input() form!: FormGroup;
}

HTML

<div [formGroup]="form">
  <input formControlName="name">
</div>

イメージとしてはこう

親コンポーネント
└─ フォーム全体を管理

子コンポーネント
└─ 入力欄の一部だけ担当

つまり、

親

= フォームの責任者

子

= 入力パーツ

という役割分担です。


なぜこれがわかりやすいのか

Angularでは、

状態を持つ場所を明確にする

のがとても重要です。

もし子コンポーネントごとに勝手にフォームを持ち始めると、

  • バリデーション
  • submit
  • 入力値取得
  • エラー表示

などが複雑になります。

なので、

フォーム本体は親が管理

に寄せたほうがシンプルです。


では、フォーム用サービスは不要?

不要ではありません。

ただし、

最初からサービス化しないほうがよい

ことが多いです。


サービス化が向いているケース

例えばこんな場合です。

  • 親子関係ではない画面同士で共有したい
  • 入力途中を保持したい
  • 画面遷移しても値を残したい
  • フォーム生成ロジックが巨大
  • 同じフォームを複数画面で再利用したい

この場合は、

FormService

を作ることがあります。


例えばこんな感じ

@Injectable({
  providedIn: 'root',
})
export class UserFormService {
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
  });
}

コンポーネント側。

constructor(public userFormService: UserFormService) {}

ただし初心者のうちは混乱しやすい

サービス化すると、

これは画面の状態なのか?

アプリ全体の状態なのか?

が曖昧になりやすいです。

なので学習初期は、

親がFormGroupを持つ
↓
子へInputで渡す

をまずしっかり理解するのがおすすめです。


Angularっぽい考え方

Angularは、

画面 = Component
データやロジック = Service

という役割分担をかなり重視しています。

フォームについても、

  • 画面ローカルな状態 → 親コンポーネント

  • アプリ全体で共有したい状態 → Service

と考えると整理しやすいです。


まとめ

複数の子コンポーネントで1つのフォームを扱う場合は、

まずはこれがおすすめです。

  • 親がFormGroupを持つ
  • 子へInputで渡す
  • 子は入力欄だけ担当する

フォーム用サービスは、

  • 大規模化
  • 状態共有
  • 再利用

が必要になってから検討すると理解しやすいです。

【Angular】`@for`で`track`が必須なのはなぜ? `*ngFor`との違いも解説

Angular 17以降では、新しい制御フロー構文として @for や @if が追加されました。

例えば一覧表示はこんな感じで書けます。

@for (food of foods; track food) {
  <li>{{ food }}</li>
}

初めて見ると、

なんで `track food` が必要なの?

と思う方も多いはず。

実際、これを消すとエラーになります。

今回は、

  • track とは何か
  • なぜ必須なのか
  • track $index との違い
  • *ngFor との違い

を初心者向けに整理します。


従来の *ngFor

Angularでは長年、一覧表示はこの書き方でした。

<li *ngFor="let food of foods">
  {{ food }}
</li>

これは今でも大量に使われています。


Angular 17以降の @for

新しい書き方はこちら。

@for (food of foods; track food) {
  <li>{{ food }}</li>
}

JavaScriptの for に近く、かなり読みやすくなっています。


track を消すとエラーになる

例えばこう書くと…

@for (food of foods) {
  <li>{{ food }}</li>
}

Angularはエラーを出します。


track は何をしている?

簡単に言うと、

「どのデータがどのHTMLか」

をAngularに教えるためのものです。


例えばこんな配列

foods = ['ラーメン', 'カレー', '寿司'];

を表示しているとします。

その後、

foods = ['ラーメン', 'うどん', 'カレー', '寿司'];

に変わった場合、

Angularは画面を書き換える必要があります。


track がないと何が困る?

Angularが、

「どの要素が同じで、どれが新しいのか」

判断しづらくなります。

結果として、

  • 全部描画し直す
  • DOM更新が増える
  • パフォーマンス悪化

につながります。


track food の意味

@for (food of foods; track food)

これは、

food の値そのものを識別IDとして使う

という意味。

つまり:

'ラーメン'
'カレー'

などの文字列をそのまま目印にしています。


実務で多いのは id

実際の開発ではオブジェクト配列が多いです。

users = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' }
];

この場合は:

@for (user of users; track user.id) {
  <li>{{ user.name }}</li>
}

のように書きます。

これが実務で最もよく見る形です。


track $index も使える

単純な練習なら、

@for (food of foods; track $index) {
  <li>{{ food }}</li>
}

でもOKです。


track food と track $index の違い

track food

値そのもので識別。

['ラーメン', 'カレー']

のような単純配列向け。


track $index

配列の順番で識別。

0番目
1番目
2番目

という番号管理。

初心者の練習ではかなり使いやすいです。


なぜAngularは必須にしたの?

昔の *ngFor には trackBy がありました。

<li *ngFor="let user of users; trackBy: trackById">

ただ、実際には省略されることが多く、

気づかないうちにパフォーマンスが悪化

するケースがありました。

そのためAngular 17以降の @for では、

最初から track を書いてもらう

設計になっています。


Angular学習としてはどうするべき?

おすすめはこの順番。

  • まず *ngFor / *ngIf を理解
  • その後 @for / @if を覚える

理由は、実務ではまだ *ngFor が大量に残っているからです。

ただ、Angularの未来としては @for 系がかなり重要になっていくと思われます。


まとめ

track は、

「このデータを識別するための目印」

です。

Angularに名札をつけてあげるイメージ。

最初は難しく感じますが、

track item.id

を見たら、

「ああ、このIDで管理してるんだな」

と思えればOKです。

【Angular】declarationsとprovidersの違いを本質から理解する

Angularのモジュールで必ず出てくるこの2つ👇

@NgModule({
  declarations: [],
  providers: []
})

なんとなく使い分けているけど

  • 何が違うの?
  • なぜ分かれているの?

この記事では 本質からシンプルに理解する。


結論(まずここだけ)

  • declarations 👉 テンプレートで使うUI部品

  • providers 👉 DIで使うサービス(ロジック)

👉 「見た目」と「ロジック」で分かれている


declarationsとは(宣言の世界)

まず英語から👇

  • declaration = 宣言

つまり

👉 「このモジュールで使うUIを宣言する場所」

対象

  • コンポーネント
  • ディレクティブ
  • パイプ

例👇

@NgModule({
  declarations: [UserComponent]
})

これは

👉「UserComponentを使います」と宣言している


declarationsが必要な理由

Angularはコンパイル時に

👉「このタグは何?」 を解決する必要がある

例えば👇

<app-user></app-user>

これを見たAngularは

👉「app-userって何?」

となる

そこで

declarations: [UserComponent]

👉「それはUserComponentだよ」と教えている


providersとは(サービスの世界)

providers は👇

👉 サービスをDI(依存性注入)するための設定

対象

  • Serviceクラス
  • DI対象のクラス

例👇

@Injectable()
export class UserService {}
@NgModule({
  providers: [UserService]
})

👉 Angularがインスタンスを管理してくれる


providersが必要な理由

Angularは

👉「必要なときにサービスを注入する」

仕組み(DI)を持っている

constructor(private userService: UserService) {}

このとき

👉 providersに登録されていないと使えない


なぜ分かれているのか(ここが本質)

一言でいうと👇

👉 コンパイルと実行の役割が違うから


declarations
  • コンパイル時に使う
  • テンプレート解析
  • UIの定義

providers
  • 実行時に使う
  • インスタンス生成
  • ロジックの供給

もし一緒だったらどうなる?

  • コンポーネントをDI対象にする?
  • サービスをテンプレートで使う?
  • 役割がごちゃ混ぜ

👉 カオスになる


イメージで理解

  • declarations 👉 部品リスト(UI)

  • providers 👉 工場(インスタンス生成)

👉 まったく別の責務


最近のAngular事情

最近は👇

@Injectable({ providedIn: 'root' })

これでprovidersを書かないことも多い

👉 でも内部的には同じ(DI)


まとめ

  • declarations

    • UI(コンポーネントなど)
    • コンパイル時に必要
  • providers

    • サービス(ロジック)
    • 実行時に必要

👉 見た目とロジックを分離するための設計

CSS Gridで「行ごとに下線」を引く方法(ハマりポイント解説)

CSS Gridで「ラベル + 値」のレイアウトを作ったとき、

名前   田中
年齢   30

👉 これに「行ごとに下線を引きたい」と思うことがある。

でもここ、ちょっとハマりポイントがある。


前提(よくあるGridレイアウト)

.kv {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 8px 16px;
}
<div class="kv">
  <div>名前</div>
  <div>田中</div>

  <div>年齢</div>
  <div>30</div>
</div>

❌ NGパターン(よくやるミス)

.kv > div {
  border-bottom: 1px solid #ddd;
}

👉 結果

名前────
田中────
年齢────
30─────

👉 1セルごとに線が引かれてしまう


✔️ 方法①:value側だけに線を引く(シンプル)

.value {
  border-bottom: 1px solid #ddd;
  padding-bottom: 4px;
}
<div class="kv">
  <div class="label">名前</div>
  <div class="value">田中</div>

  <div class="label">年齢</div>
  <div class="value">30</div>
</div>

👉 見た目

名前   田中────
年齢   30─────

メリット

  • シンプル
  • そのままGridが使える

デメリット

  • 左側(ラベル)には線がない

✔️ 方法②:行ごとに線を引く(ちゃんとやる)

👉 行のラッパーを作る

HTML

<div class="kv">
  <div class="row">
    <div class="label">名前</div>
    <div class="value">田中</div>
  </div>

  <div class="row">
    <div class="label">年齢</div>
    <div class="value">30</div>
  </div>
</div>

CSS

.kv {
  display: flex;
  flex-direction: column;
}

.row {
  display: grid;
  grid-template-columns: 120px 1fr;
  border-bottom: 1px solid #ddd;
  padding: 8px 0;
}

👉 見た目

名前   田中
───────
年齢   30
───────

メリット

  • 行全体に線が引ける
  • UIとして綺麗

デメリット

  • HTMLが少し増える

✔️ なぜこうなるのか

👉 Gridには「行」というDOM構造がない

  • Gridは「セルの集合」
  • 行は見た目上の概念

👉 だから

  • セル単位のスタイル → 簡単
  • 行単位のスタイル → 工夫が必要

✔️ どっちを使うべき?

  • シンプルなUI → 方法①
  • しっかり区切りたい → 方法②

✔️ まとめ

  • Gridは便利だけど「行スタイル」は弱い
  • 行単位で装飾したいならラッパーを作る
  • NGパターンに注意

👉 「行を扱いたいならdivを1枚かませる」

CSS Gridの「fr」とは?割合でレイアウトを組む基本

CSS Gridを触っているとよく出てくるのが fr という単位。

最初は「これ何?」となりがちだけど、理解するとレイアウトが一気に楽になる。

frとは何か

fr は fraction(フラクション)=割合 の略。

👉 残りスペースを分割する単位

これがポイント。


基本の使い方

左固定 + 右可変

grid-template-columns: 120px 1fr;

これはこういう意味:

  • 左:120px固定
  • 右:残り全部

よくある「ラベル + 値」のレイアウトに最適。


均等割り

grid-template-columns: 1fr 1fr;
  • 残りスペースを半分ずつ

比率を変える

grid-template-columns: 1fr 2fr;
  • 左:1
  • 右:2(2倍の幅)

👉 「割合で分ける」イメージ


pxとfrの違い

  • px → 固定サイズ
  • fr → 可変サイズ(残りの分配)

組み合わせると強い:

grid-template-columns: 200px 1fr 1fr;
  • 左:200px固定
  • 残りを2つで分割

昔のやり方との違い

昔はこうしていた:

width: calc(100% - 200px);
  • 計算が必要
  • レイアウト崩れやすい

今は:

grid-template-columns: 200px 1fr;

👉 シンプルで壊れにくい


よくある実用パターン

ラベルと値を揃える

.kv {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 8px 16px;
}
<div class="kv">
  <div>名前</div>
  <div>田中</div>

  <div>年齢</div>
  <div>30</div>
</div>

👉 縦位置がピシッと揃う


6分割したいときの注意点

ここはよく間違えるポイント。

❌ 間違い

grid-template-columns: 6fr;

👉 これは「1列」しかできない (幅が大きいだけ)


✅ 正しい書き方

grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;

👉 6列に均等分割される


✅ 実務でよく使う書き方

grid-template-columns: repeat(6, 1fr);

👉 同じ指定をまとめて書ける


frの正しい理解

  • fr は「数」ではない
  • 比率(重み)

例えば:

grid-template-columns: 1fr 6fr;
  • 左:1
  • 右:6(6倍広い)

まとめ

  • fr は fraction(割合)
  • 残りスペースを分配する単位
  • pxと組み合わせると強い
  • 分割したいときは repeat() が便利

👉 「固定 + 可変」を簡単に書けるのが最大のメリット