Angular's Resource API provides a declarative way to manage asynchronous data loading with built-in loading states, error handling, and server-side rendering support. Resources offer a more structured approach to data fetching compared to manual observable management.
Create resources for managing asynchronous data with automatic state management.
/**
* Creates a resource with a loader function
* @param loader - Function that returns data or Promise/Observable
* @param options - Optional configuration
* @returns Resource instance
*/
function resource<T>(
loader: ResourceLoader<T>,
options?: ResourceOptions
): WritableResource<T>;
/**
* Creates a resource with request/loader configuration
* @param config - Resource configuration object
* @returns Resource instance
*/
function resource<T, R>(config: {
request: () => R;
loader: ResourceLoader<T, R>;
}): WritableResource<T>;
/**
* Function type for resource loaders
*/
type ResourceLoader<T, R = unknown> = (request: R) => T | Promise<T> | Observable<T>;
/**
* Configuration options for resources
*/
interface ResourceOptions {
/** Injector for dependency injection context */
injector?: Injector;
/** Whether to load the resource on creation */
manualLoading?: boolean;
/** Equal function for comparing request values */
equal?: (a: unknown, b: unknown) => boolean;
}Core interfaces for working with resources and their state.
/**
* Read-only resource interface
*/
interface Resource<T> {
/** Current resource value */
value(): T | undefined;
/** Current resource status */
status(): ResourceStatus;
/** Current error if any */
error(): unknown;
/** Whether resource is currently loading */
isLoading(): boolean;
/** Whether resource has a value */
hasValue(): boolean;
}
/**
* Writable resource interface with reload capability
*/
interface WritableResource<T> extends Resource<T> {
/** Reload the resource */
reload(): void;
/** Set resource to loading state */
set loading(): void;
/** Set resource value directly */
set(value: T): void;
/** Update resource value using function */
update(updateFn: (current: T | undefined) => T): void;
/** Destroy the resource and cleanup subscriptions */
destroy(): void;
}
/**
* Resource reference for advanced use cases
*/
interface ResourceRef<T> extends WritableResource<T> {
/** Original request used to create resource */
request(): unknown;
/** Resource loader function */
loader(): ResourceLoader<T>;
}
/**
* Resource status enumeration
*/
type ResourceStatus = 'idle' | 'loading' | 'resolved' | 'error' | 'local';Utilities for managing resource state and lifecycle.
/**
* Check if a value is a resource
* @param value - Value to check
* @returns True if value is a resource
*/
function isResource(value: unknown): value is Resource<unknown>;
/**
* Create a local resource with immediate value
* @param value - Initial value for the resource
* @returns Resource with local status
*/
function localResource<T>(value: T): WritableResource<T>;
/**
* Transform a resource value using a mapping function
* @param source - Source resource to transform
* @param transform - Transformation function
* @returns New resource with transformed values
*/
function mapResource<T, U>(
source: Resource<T>,
transform: (value: T) => U
): Resource<U>;
/**
* Combine multiple resources into a single resource
* @param resources - Array of resources to combine
* @returns Resource containing array of all values
*/
function combineResources<T extends readonly Resource<any>[]>(
resources: T
): Resource<{[K in keyof T]: T[K] extends Resource<infer U> ? U : never}>;Utilities for managing resources in server-side rendering contexts.
/**
* Preload resource data during SSR
* @param resource - Resource to preload
* @returns Promise that resolves when resource is loaded
*/
function preloadResource<T>(resource: Resource<T>): Promise<T>;
/**
* Mark resource as server-only (won't execute on client)
* @param resource - Resource to mark as server-only
* @returns Modified resource configuration
*/
function serverOnlyResource<T>(resource: WritableResource<T>): WritableResource<T>;
/**
* Resource hydration options for client-side
*/
interface ResourceHydrationOptions {
/** Whether to revalidate data on client hydration */
revalidate?: boolean;
/** Timeout for server-side resource loading */
timeout?: number;
}import { Component, resource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-list',
template: `
<div>
<h3>User List</h3>
<div [ngSwitch]="usersResource.status()">
<div *ngSwitchCase="'loading'">
<p>Loading users...</p>
</div>
<div *ngSwitchCase="'error'">
<p>Error: {{usersResource.error()}}</p>
<button (click)="usersResource.reload()">Retry</button>
</div>
<div *ngSwitchCase="'resolved'">
<ul>
<li *ngFor="let user of usersResource.value()">
{{user.name}} - {{user.email}}
</li>
</ul>
<button (click)="usersResource.reload()">Refresh</button>
</div>
</div>
</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Resource that automatically loads user data
usersResource = resource<User[]>(() =>
this.http.get<User[]>('/api/users')
);
}import { Component, signal, resource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface UserDetails {
id: number;
name: string;
email: string;
profile: {
bio: string;
avatar: string;
joinDate: string;
};
}
@Component({
selector: 'app-user-details',
template: `
<div>
<h3>User Details</h3>
<select (change)="selectUser($event)">
<option value="">Select a user</option>
<option value="1">Alice</option>
<option value="2">Bob</option>
<option value="3">Charlie</option>
</select>
<div *ngIf="selectedUserId()" [ngSwitch]="userDetailsResource.status()">
<div *ngSwitchCase="'loading'">
<p>Loading user details...</p>
</div>
<div *ngSwitchCase="'error'">
<p>Failed to load user details</p>
<button (click)="userDetailsResource.reload()">Retry</button>
</div>
<div *ngSwitchCase="'resolved'">
<div class="user-profile">
<img [src]="userDetailsResource.value()?.profile.avatar"
[alt]="userDetailsResource.value()?.name">
<h4>{{userDetailsResource.value()?.name}}</h4>
<p>{{userDetailsResource.value()?.email}}</p>
<p>{{userDetailsResource.value()?.profile.bio}}</p>
<p>Joined: {{userDetailsResource.value()?.profile.joinDate | date}}</p>
</div>
</div>
</div>
</div>
`
})
export class UserDetailsComponent {
private http = inject(HttpClient);
// Signal for selected user ID
selectedUserId = signal<string | null>(null);
// Resource that reloads when selectedUserId changes
userDetailsResource = resource<UserDetails, string>({
request: () => this.selectedUserId(),
loader: (userId) => {
if (!userId) {
throw new Error('No user selected');
}
return this.http.get<UserDetails>(`/api/users/${userId}`);
}
});
selectUser(event: Event): void {
const target = event.target as HTMLSelectElement;
this.selectedUserId.set(target.value || null);
}
}import { Component, resource, computed, mapResource, combineResources } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface RawWeatherData {
temperature_celsius: number;
humidity_percent: number;
conditions: string;
}
interface ProcessedWeatherData {
temperature: {
celsius: number;
fahrenheit: number;
};
humidity: number;
conditions: string;
comfort: 'comfortable' | 'hot' | 'cold' | 'humid';
}
interface LocationData {
name: string;
country: string;
timezone: string;
}
@Component({
selector: 'app-weather-dashboard',
template: `
<div>
<h3>Weather Dashboard</h3>
<div [ngSwitch]="combinedData.status()">
<div *ngSwitchCase="'loading'">
<p>Loading weather data...</p>
</div>
<div *ngSwitchCase="'error'">
<p>Error loading data</p>
<button (click)="refreshData()">Retry</button>
</div>
<div *ngSwitchCase="'resolved'">
<div class="weather-card">
<h4>{{combinedData.value()?.location.name}}, {{combinedData.value()?.location.country}}</h4>
<div class="temperature">
<span class="temp-c">{{combinedData.value()?.weather.temperature.celsius}}°C</span>
<span class="temp-f">({{combinedData.value()?.weather.temperature.fahrenheit}}°F)</span>
</div>
<p>Conditions: {{combinedData.value()?.weather.conditions}}</p>
<p>Humidity: {{combinedData.value()?.weather.humidity}}%</p>
<p class="comfort" [class]="combinedData.value()?.weather.comfort">
Comfort: {{combinedData.value()?.weather.comfort}}
</p>
<button (click)="refreshData()">Refresh</button>
</div>
</div>
</div>
</div>
`,
styles: [`
.weather-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
max-width: 400px;
}
.temperature {
font-size: 1.5em;
margin: 8px 0;
}
.temp-f {
color: #666;
font-size: 0.8em;
}
.comfort.comfortable { color: green; }
.comfort.hot { color: red; }
.comfort.cold { color: blue; }
.comfort.humid { color: orange; }
`]
})
export class WeatherDashboardComponent {
private http = inject(HttpClient);
// Raw weather data resource
private rawWeatherResource = resource<RawWeatherData>(() =>
this.http.get<RawWeatherData>('/api/weather/current')
);
// Location data resource
private locationResource = resource<LocationData>(() =>
this.http.get<LocationData>('/api/location/current')
);
// Transform raw weather data
private processedWeatherResource = mapResource(
this.rawWeatherResource,
(raw): ProcessedWeatherData => ({
temperature: {
celsius: raw.temperature_celsius,
fahrenheit: Math.round((raw.temperature_celsius * 9/5) + 32)
},
humidity: raw.humidity_percent,
conditions: raw.conditions,
comfort: this.calculateComfort(raw.temperature_celsius, raw.humidity_percent)
})
);
// Combine both resources
combinedData = combineResources([
this.processedWeatherResource,
this.locationResource
]).pipe(
mapResource(([weather, location]) => ({ weather, location }))
);
private calculateComfort(temp: number, humidity: number): ProcessedWeatherData['comfort'] {
if (temp >= 30) return 'hot';
if (temp <= 10) return 'cold';
if (humidity >= 70) return 'humid';
return 'comfortable';
}
refreshData(): void {
this.rawWeatherResource.reload();
this.locationResource.reload();
}
}import { Component, signal, resource, localResource } from '@angular/core';
interface Task {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
@Component({
selector: 'app-task-manager',
template: `
<div>
<h3>Task Manager</h3>
<div class="add-task">
<input
#taskInput
[(ngModel)]="newTaskTitle"
placeholder="Enter task title"
(keyup.enter)="addTask()"
>
<button (click)="addTask()" [disabled]="!newTaskTitle.trim()">
Add Task
</button>
</div>
<div class="task-list">
<div *ngFor="let task of tasksResource.value()" class="task-item">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task.id)"
>
<span [class.completed]="task.completed">{{task.title}}</span>
<button (click)="deleteTask(task.id)">Delete</button>
</div>
</div>
<div class="stats">
<p>Total: {{totalTasks()}}</p>
<p>Completed: {{completedTasks()}}</p>
<p>Pending: {{pendingTasks()}}</p>
</div>
<div class="actions">
<button (click)="clearCompleted()">Clear Completed</button>
<button (click)="loadSampleTasks()">Load Sample Tasks</button>
</div>
</div>
`,
styles: [`
.task-item {
display: flex;
align-items: center;
gap: 8px;
margin: 4px 0;
}
.completed {
text-decoration: line-through;
color: #666;
}
.add-task {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.stats {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
margin: 16px 0;
}
.actions {
display: flex;
gap: 8px;
}
`]
})
export class TaskManagerComponent {
newTaskTitle = '';
// Local resource for managing tasks in memory
tasksResource = localResource<Task[]>([]);
// Computed properties from resource
totalTasks = computed(() => this.tasksResource.value()?.length ?? 0);
completedTasks = computed(() =>
this.tasksResource.value()?.filter(task => task.completed).length ?? 0
);
pendingTasks = computed(() =>
this.tasksResource.value()?.filter(task => !task.completed).length ?? 0
);
addTask(): void {
const title = this.newTaskTitle.trim();
if (!title) return;
const newTask: Task = {
id: Date.now(),
title,
completed: false,
createdAt: new Date()
};
this.tasksResource.update(current => [...(current ?? []), newTask]);
this.newTaskTitle = '';
}
toggleTask(taskId: number): void {
this.tasksResource.update(current =>
current?.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
) ?? []
);
}
deleteTask(taskId: number): void {
this.tasksResource.update(current =>
current?.filter(task => task.id !== taskId) ?? []
);
}
clearCompleted(): void {
this.tasksResource.update(current =>
current?.filter(task => !task.completed) ?? []
);
}
loadSampleTasks(): void {
const sampleTasks: Task[] = [
{ id: 1, title: 'Learn Angular Resources', completed: false, createdAt: new Date() },
{ id: 2, title: 'Build a todo app', completed: true, createdAt: new Date() },
{ id: 3, title: 'Write documentation', completed: false, createdAt: new Date() }
];
this.tasksResource.set(sampleTasks);
}
}import { Component, resource, preloadResource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID, inject } from '@angular/core';
interface Article {
id: number;
title: string;
content: string;
author: string;
publishedAt: string;
}
@Component({
selector: 'app-article',
template: `
<article>
<div [ngSwitch]="articleResource.status()">
<div *ngSwitchCase="'loading'">
<div class="skeleton">
<div class="skeleton-title"></div>
<div class="skeleton-content"></div>
</div>
</div>
<div *ngSwitchCase="'resolved'">
<header>
<h1>{{articleResource.value()?.title}}</h1>
<p class="meta">
By {{articleResource.value()?.author}} |
{{articleResource.value()?.publishedAt | date}}
</p>
</header>
<div class="content" [innerHTML]="articleResource.value()?.content">
</div>
</div>
<div *ngSwitchCase="'error'">
<div class="error">
<h2>Article Not Found</h2>
<p>The requested article could not be loaded.</p>
<button (click)="articleResource.reload()">Try Again</button>
</div>
</div>
</div>
</article>
`,
styles: [`
.skeleton {
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-title {
height: 32px;
background: #e2e8f0;
border-radius: 4px;
margin-bottom: 16px;
}
.skeleton-content {
height: 200px;
background: #e2e8f0;
border-radius: 4px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.meta {
color: #666;
font-size: 0.9em;
}
.content {
line-height: 1.6;
margin-top: 24px;
}
.error {
text-align: center;
padding: 48px 16px;
}
`]
})
export class ArticleComponent {
private http = inject(HttpClient);
private platformId = inject(PLATFORM_ID);
// Get article ID from route or input
articleId = signal(1);
// Resource with SSR optimization
articleResource = resource<Article, number>({
request: () => this.articleId(),
loader: async (id) => {
// Preload on server for faster initial render
if (isPlatformServer(this.platformId)) {
const article = await this.http.get<Article>(`/api/articles/${id}`).toPromise();
if (article) {
// Cache for client hydration
return article;
}
}
return this.http.get<Article>(`/api/articles/${id}`);
}
});
ngOnInit(): void {
// Preload resource data during SSR
if (isPlatformServer(this.platformId)) {
preloadResource(this.articleResource);
}
}
}