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
}