PAYATHON 2026

Angular 导航至 transactions 子页面后,为什么数据需要刷新才显示?

排障赵师傅

结论

首次进入页面时,如果控制台的 Transactions loaded: 已经打印出交易记录,表格却仍为空,应先检查 Angular 的变更检测。此时数据已经赋给组件,但视图没有及时更新。将 transactions 改为模板直接读取的 Signal,可以解决这类缺少更新通知的问题。

如果首次请求返回的是空数组,就要检查创建请求是否完成,以及查询接口是否返回了新记录。调整变更检测无法解决响应中没有数据的问题。

目前缺少 Angular 版本、启动配置和路由配置,仅凭这些代码还无法确定具体原因,也不能直接归因于 Postgres 延迟。

为什么再次点击后才显示

列表组件在初始化时调用 loadTransactions()。由于 HTTP 请求是异步的,页面可能先按空数组完成渲染,之后才执行:

this.transactions = [...data];

这段赋值更新了 JavaScript 属性。页面能否及时显示数据,还取决于 Angular 是否对相应视图执行了变更检测。

如果应用使用 Zone.js 和默认变更检测策略,且请求处于正常的 Angular 执行上下文中,这类 HttpClient 回调通常能正常更新页面。如果项目采用无 Zone 的变更检测,或视图受 OnPush 等配置影响,就需要检查异步更新是否发出了视图更新通知。

代码中虽然声明了:

loading = signal(false);

但这不会让 transactions 自动成为响应式状态。所贴模板也没有读取 loading(),因此不能依靠 loading.set(false) 通知该模板更新。

再次点击导航链接,可能触发了一次视图检查,让先前已经赋值的数据显示出来。F5 则会重新初始化应用并发起请求。这些现象有助于排查,但还不能据此确定原因。

先确认首次请求返回了什么

打开浏览器开发者工具,不刷新页面,完成一次“创建交易、进入列表”的操作,并检查:

  1. 是否发出了 GET http://localhost:3000/transactions。
  2. 响应中是否已经包含新交易。
  3. 控制台的 Transactions loaded: 是否打印了这些记录。

根据结果选择排查方向:

首次进入页面时的结果优先检查
没有发送 GET 请求路由是否进入列表组件、ngOnInit() 是否执行、组件是否被复用
GET 返回空数组或缺少新交易POST 与 GET 的先后顺序、后端查询条件、写入提交时机
GET 返回记录,回调已执行,表格仍为空变更检测通知、模板错误、列表追踪键

问题描述中同时出现了 /users 和 /transactions,还需核对实际导航地址是否对应 TransactionList。

将列表数据改为 Signal

确认请求返回了正确数据后,可以将交易列表改为 Signal,让模板直接读取它。组件可改为:

import { CommonModule } from '@angular/common';
import { Component, inject, OnInit, signal } from '@angular/core';
import { finalize } from 'rxjs';
import { TransactionsService } from '../../../core/services/transactions';
import { Transaction } from '../../../core/models/transaction.model';

@Component({
  selector: 'app-transaction-list',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './transaction-list.html',
  styleUrl: './transaction-list.scss',
})
export class TransactionList implements OnInit {
  private readonly transactionsService = inject(TransactionsService);

  readonly transactions = signal<Transaction[]>([]);
  readonly loading = signal(false);
  readonly errorMessage = signal('');

  ngOnInit(): void {
    this.loadTransactions();
  }

  loadTransactions(): void {
    this.loading.set(true);
    this.errorMessage.set('');

    this.transactionsService
      .getTransactions()
      .pipe(finalize(() => this.loading.set(false)))
      .subscribe({
        next: (data) => {
          this.transactions.set(data);
        },
        error: (error) => {
          console.error('Error loading transactions:', error);
          this.errorMessage.set('交易记录加载失败,请重试。');
        },
      });
  }
}

模板也要同步改为读取 transactions(),否则会与组件中的声明不匹配。

@if (loading()) {
  <p>正在加载交易记录……</p>
} @else if (errorMessage()) {
  <p>{{ errorMessage() }}</p>
} @else {
  <table class="transactions-table">
    <thead>
      <tr>
        <th>Názov</th>
        <th>Typ</th>
        <th>Suma</th>
        <th>Kategória</th>
        <th>Zrealizovaná dňa</th>
        <th>Popis</th>
      </tr>
    </thead>
    <tbody>
      @for (t of transactions(); track t) {
        <tr>
          <td>{{ t.name }}</td>
          <td>
            <span [ngClass]="t.type">
              {{ t.type === 'income' ? 'Príjem' : 'Výdavok' }}
            </span>
          </td>
          <td>{{ t.amount }} €</td>
          <td>{{ t.category }}</td>
          <td>{{ t.completedAt | date: 'dd. MM. yyyy' }}</td>
          <td>{{ t.description }}</td>
        </tr>
      } @empty {
        <tr>
          <td colspan="6">暂无交易记录。</td>
        </tr>
      }
    </tbody>
  </table>
}

Angular 会跟踪模板读取的 Signal,调用 this.transactions.set(data) 时便会收到更新相关视图的通知。

服务接口可以保持原样。所贴的 TransactionsService 每次订阅都会执行 HTTP 请求,代码中没有缓存逻辑,目前没有依据要求清理服务缓存。

创建完成后再导航

提交表单后立即手动切换页面,可能导致 GET 在 POST 完成前发出。如果需要自动跳转,应在创建请求的成功回调中导航。

在创建组件中增加:

import { Router } from '@angular/router';

并注入:

private readonly router = inject(Router);

然后在现有成功回调末尾添加导航:

next: (transaction) => {
  this.transactionCreated.emit(transaction);

  this.form.reset({
    transactionName: '',
    transactionType: 'income',
    transactionAmount: '',
    transactionCategory: '',
    transactionDate: '',
    transactionDescription: '',
  });

  this.loading.set(false);
  void this.router.navigate(['/transactions']);
},

这样,导航会在前端收到 POST 成功响应后发生。后端也应等写入成功、事务提交完成后再返回成功响应;如果接口提前返回,仍需修正后端的执行时序。

如果 transactionCreated 的父组件处理函数已经负责导航,应将跳转统一放在一处。

其他需要调整的细节

不要用交易金额作为追踪键

原模板使用:

@for (t of transactions; track t.amount)

不同交易可能有相同金额,amount 因而不适合作为唯一追踪键,可能引发重复键错误或 DOM 复用异常。

上面的示例使用 track t,按对象身份追踪。如果模型中有稳定且唯一的 id,应优先使用:

@for (t of transactions(); track t.id)

题目没有提供 Transaction 模型,不能假定它有 id。当前示例可以按对象身份追踪,但重新请求生成新对象后,这种方式对已有行的复用效果不如稳定 ID。

EventEmitter 不会自动通知另一个路由页面

this.transactionCreated.emit(transaction);

只有显式监听该输出的消费者才会收到事件。另一个路由中的列表组件不会因此自动重新请求数据。

如果进入列表时创建了新的组件实例,现有的 ngOnInit() 就会负责加载。如果项目通过自定义路由复用策略保留列表实例,则需另行安排重新加载的时机,因为每次显示该实例时,ngOnInit() 不会重复执行。

保留普通数组时,可以显式标记视图更新

如果暂时不用 Signal,可以注入 ChangeDetectorRef,在修改列表后调用:

this.transactions = data;
this.loading.set(false);
this.cdr.markForCheck();

其中,cdr 声明为:

private readonly cdr = inject(ChangeDetectorRef);

同时需要从 @angular/core 导入 ChangeDetectorRef。如果错误回调也修改了页面展示状态,同样应发出更新通知。

markForCheck() 适用于需要显式通知 Angular 检查视图的情况。先确认首次 GET 的响应,再判断要修复的是数据读取时序还是视图更新,避免用固定延时或强制刷新掩盖问题。

备注:内容仅供参考。