Resource API & linkedSignal (v19)
📚 Resource API
resource() is Angular's built-in async data loading pattern:
- Declarative: Define request as signal, auto-refetches on change
- States: Tracks idle, loading, resolved, error states
- Signal-based: Returns signal of data
- Auto-cancellation: Cancels previous request on new params
- httpResource(): Built-in HTTP integration (v20+)
📚 linkedSignal
linkedSignal() creates a writable computed signal:
- Writable computed: Like computed but can be set manually
- Auto-resets: Resets to computed value when dependencies change
- User overrides: User can override, but source change resets it
- Use cases: Form defaults, derived state with overrides
🎯 Interview Questions
- Q1: What is the difference between resource() and httpResource()?
- A: resource() is generic for any async loader. httpResource() is specifically for HTTP requests with built-in HttpClient integration.
- Q2: What states does a resource have?
- A: idle, loading, resolved (success), error. Access via resource.status().
- Q3: What is linkedSignal used for?
- A: Creating writable derived state. It computes from sources but can be manually overwritten. Resets when source changes.
- Q4: How does resource handle concurrent requests?
- A: New requests cancel pending ones automatically. Uses AbortController internally.
🔧 Live Demo - linkedSignal
Product Configurator
$29.99
Change product → price auto-resets. Override price → persists until product changes.
$29.99
🔧 Resource API Concept
Simulated Resource Loading
Enter a user ID and click Load
💻 Resource & linkedSignal Code
import { resource, linkedSignal, signal, computed } from '@angular/core';
// ===== resource() - Async Data Loading (v19+) =====
@Component({...})
export class UserComponent {
userId = signal(1);
// resource auto-fetches when userId changes
userResource = resource({
request: () => ({ id: this.userId() }),
loader: async ({ request, abortSignal }) => {
const response = await fetch(
`/api/users/${request.id}`,
{ signal: abortSignal }
);
return response.json();
}
});
// Access resource state
// this.userResource.value() // The data
// this.userResource.status() // 'idle' | 'loading' | 'resolved' | 'error'
// this.userResource.error() // Error if failed
// this.userResource.isLoading() // boolean shortcut
}
// Template usage:
// @if (userResource.isLoading()) {
// <spinner />
// }
// @if (userResource.value(); as user) {
// <p>{{ user.name }}</p>
// }
// ===== httpResource() - HTTP-specific (v20+) =====
import { httpResource } from '@angular/common/http';
@Component({...})
export class ProductComponent {
productId = signal(1);
// Simplified HTTP resource
product = httpResource<Product>({
url: () => `/api/products/${this.productId()}`,
});
// With options
products = httpResource<Product[]>({
url: '/api/products',
method: 'GET',
headers: { 'Accept': 'application/json' },
});
}
// ===== linkedSignal() - Writable Computed (v19+) =====
@Component({...})
export class ConfigComponent {
// Source signal
selectedSize = signal<'sm' | 'md' | 'lg'>('md');
// Size to pixels mapping
sizeToPx = { sm: 12, md: 16, lg: 20 };
// linkedSignal: derived but writable!
fontSize = linkedSignal(() => this.sizeToPx[this.selectedSize()]);
// User can override fontSize manually
setCustomSize(px: number) {
this.fontSize.set(px); // Allowed!
}
// But when selectedSize changes, fontSize resets to computed value
changeSize(size: 'sm' | 'md' | 'lg') {
this.selectedSize.set(size);
// fontSize automatically resets!
}
}
// ===== linkedSignal with source option =====
// Alternative syntax with explicit source/computation
const derivedValue = linkedSignal({
source: someSignal,
computation: (sourceValue, previous) => {
// Can access previous value!
return sourceValue * 2;
}
});
// ===== Real World Example: Form Defaults =====
@Component({...})
export class OrderForm {
selectedProduct = signal<Product | null>(null);
// Default quantity based on product, but user can change
quantity = linkedSignal(() =>
this.selectedProduct()?.minQuantity ?? 1
);
// Default notes based on product
notes = linkedSignal(() =>
this.selectedProduct()?.defaultNotes ?? ''
);
// When user selects new product, form resets to defaults
// But user's manual edits persist until product changes
}