Interview question
Defer a heavy Angular chart and test every loading state
Builds an interaction-triggered deferrable view with a stable accessible placeholder and deterministic tests for placeholder, loading, error, and completion.
TL;DR
Builds an interaction-triggered deferrable view with a stable accessible placeholder and deterministic tests for placeholder, loading, error, and completion.
Angular @defer, interaction triggers, prefetch, placeholders, loading and error states, DeferBlockBehavior, layout stability, and accessibility.
Practice the problem like a real interview: restate, reason, implement, and test.
Place a heavy analytics chart behind an interaction-triggered @defer block. Prefetch during idle, keep a fixed-size placeholder to prevent layout shift, and provide accessible loading and error states. Write a manual defer-block test that renders placeholder, loading, error, and complete states without downloading a real chart chunk.
initial -> placeholder with "Load chart" button
interaction -> loading status
chunk succeeds -> chart renders
chunk fails -> visible retry guidance
@defer with interaction and idle-prefetch triggers.import {Component} from '@angular/core';
import {DeferBlockBehavior, DeferBlockState, TestBed} from '@angular/core/testing';
@Component({
imports: [HeavyChart],
styles: [`.chart-shell { min-height: 20rem; }`],
template: `
<section class="chart-shell" aria-labelledby="chart-title">
<h2 id="chart-title">Request volume</h2>
@defer (on interaction; prefetch on idle) {
<app-heavy-chart />
} @placeholder {
<button type="button">Load chart</button>
} @loading {
<p role="status">Loading chart…</p>
} @error {
<p role="alert">The chart could not load. Try again later.</p>
}
</section>
`,
})
class AnalyticsPanel {}
it('exposes every deferred state', async () => {
TestBed.configureTestingModule({
imports: [AnalyticsPanel],
deferBlockBehavior: DeferBlockBehavior.Manual,
});
await TestBed.compileComponents();
const fixture = TestBed.createComponent(AnalyticsPanel);
await fixture.whenStable();
const block = (await fixture.getDeferBlocks())[0];
expect(fixture.nativeElement.textContent).toContain('Load chart');
await block.render(DeferBlockState.Loading);
expect(fixture.nativeElement.querySelector('[role=status]')).not.toBeNull();
await block.render(DeferBlockState.Error);
expect(fixture.nativeElement.querySelector('[role=alert]')).not.toBeNull();
await block.render(DeferBlockState.Complete);
expect(fixture.nativeElement.querySelector('app-heavy-chart')).not.toBeNull();
});
The wrapper has constant client work; the chart code and rendering cost move out of the initial bundle until the trigger.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.