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 则会重新初始化应用并发起请求。这些现象有助于排查,但还不能据此确定原因。
先确认首次请求返回了什么
打开浏览器开发者工具,不刷新页面,完成一次“创建交易、进入列表”的操作,并检查:
- 是否发出了
GET http://localhost:3000/transactions。 - 响应中是否已经包含新交易。
- 控制台的
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 的响应,再判断要修复的是数据读取时序还是视图更新,避免用固定延时或强制刷新掩盖问题。
备注:内容仅供参考。
