Contents

Angular Enterprise Dashboard - Phase 3B.3: Six States of Data — ResourceStatus & the UI

Most applications handle two data states: “loading” and “loaded.” Maybe three if they remember errors. But in a real enterprise dashboard, there are six distinct states — and handling each explicitly is the difference between a polished product and a buggy one.

Every Async Operation Has Six Faces

In this post, we’ll build a UI that correctly responds to every ResourceStatus value.


Angular’s resource() exposes a status() signal with these possible values:

stateDiagram-v2
    [*] --> idle: No params
    idle --> loading: First fetch
    loading --> resolved: Success
    loading --> error: Failure
    resolved --> reloading: reload() called
    reloading --> resolved: Success
    reloading --> error: Failure
    resolved --> loading: params change
    resolved --> local: .update() / .set()
    local --> loading: params change
    local --> reloading: reload() called
    error --> loading: params change
    error --> reloading: reload() called
StatusMeaningUI Treatment
idleNo request pending (params returned undefined)Welcome message
loadingFirst fetch in progressShimmer skeleton
reloadingRe-fetch after reload() — data still visibleOverlay indicator
resolvedData successfully loadedProject cards
errorLoader threw an errorError banner + Retry
localData mutated locally via .update()Data + sync notice

Our template uses Angular’s @switch to render the appropriate UI for each state:

@switch (projectsResource.status()) { @case ('idle') {
<div class="empty-state">
  <span class="empty-icon">🔍</span>
  <p>Type a search query to find projects.</p>
</div>
} @case ('loading') {
<div class="skeleton-grid">
  @for (i of skeletonItems; track i) {
  <div class="skeleton-card"></div>
  }
</div>
} @case ('error') {
<div class="error-banner">
  <span>❌ {{ getErrorMessage(projectsResource.error()) }}</span>
  <button (click)="handleReload()">Retry</button>
</div>
} @default {
<!-- resolved / reloading / local all show data -->
<div class="project-grid">
  @for (project of projectsResource.value(); track project.id) {
  <article class="project-card">...</article>
  } @empty {
  <div class="empty-state">📭 No projects match your search.</div>
  }
</div>
} }

Why @default? The resolved, reloading, and local states all have data to show. They differ in secondary treatment (overlay, notice), not in the core content. Grouping them under @default avoids duplication.


Instead of a spinner, we use shimmer skeleton cards that hint at the shape of the content:

.skeleton-card {
  height: 200px;
  border-radius: var(--radius-lg, 16px);
  background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

Why skeletons over spinners? Skeletons reduce perceived loading time because they give the user a sense of the page’s structure. Studies show users rate skeleton-loaded pages as 10-15% faster than spinner-loaded pages with identical real load times.


Sometimes the user wants fresh data. Our “Reload” button calls projectsResource.reload():

handleReload(): void {
  this.projectsResource.reload();
}

When reload() is called:

  • Status transitions from resolvedreloading
  • The existing data stays visible (unlike loading, which clears it)
  • The loader runs again with the same params
  • On success, status returns to resolved with fresh data

We show a subtle overlay during reloading:

@if (projectsResource.status() === 'reloading') {
<div class="reload-overlay">Refreshing...</div>
}

For educational purposes (and great UX), we show the current ResourceStatus as a colored pill:

<span class="status-badge" [class]="'status-' + projectsResource.status()">
  {{ projectsResource.status() | uppercase }}
</span>

Each status gets its own color:

.status-idle {
  background: #f1f5f9;
  color: #64748b;
}
.status-loading,
.status-reloading {
  background: #ede9fe;
  color: #7c3aed;
}
.status-resolved {
  background: #ecfdf5;
  color: #059669;
}
.status-error {
  background: #fef2f2;
  color: #dc2626;
}
.status-local {
  background: #fef3c7;
  color: #d97706;
}

This pattern isn’t specific to Angular. Any async operation — in React, Vue, Svelte, or vanilla JS — goes through these states. The key principle is:

Never assume your data is “just there.” Every async operation has a lifecycle, and every state deserves explicit UI treatment.

The resource() API makes this easy because the status is a first-class signal. No more manual isLoading / hasError / data boolean juggling.


We’ve handled all six states, but there’s one more advanced pattern: optimistic updates. In Phase 3B.4, we’ll see how .update() lets you modify data locally without re-fetching — and how the 'local' status communicates this to the user.


Open your browser DevTools and watch the status badge change as you search, reload, and interact. It’s a real-time view of the resource lifecycle!

Related Content