Part 1: Foundations - Basic Resource Fetching

Angular’s resource() method offers a clean approach to managing asynchronous data loading. Let’s begin with the simplest case: retrieving weather information through the resource API. This initial example lays the groundwork for more sophisticated patterns.

We’ll build a basic component that defines a resource for data retrieval and includes a button to re-fetch the data on demand. In addition, we’ll display the loading indicator and the fetched weather details once they arrive.

The initial component code is as follows:

import { Component, resource } from '@angular/core';

interface WeatherData {
  temperature: number;
  condition: string;
  icon: string;
}

@Component({
  selector: 'app-weather-info',
  template: `
    <div class="card bg-base-200 w-96 shadow-xl mx-auto">
      <div class="card-body flex flex-col items-center gap-4">
        <button
          class="btn btn-block btn-primary btn-outline"
          (click)="weatherResource.reload()"
        >
          Get Weather Info
        </button>
        @if (weatherResource.isLoading()) {
          <span class="loading loading-spinner loading-lg"></span>
        } @else if (weatherResource.value()) {
          <img
            [src]="weatherResource.value()?.icon"
            class="w-20 object-fit"
            alt="weather icon"
          />
          <p class="text-2xl">
            Temperature: {{ weatherResource.value()?.temperature }}
          </p>
          <p class="text-xl">
            Condition: {{ weatherResource.value()?.condition }}
          </p>
        }
      </div>
    </div>
  `,
})
export class WeatherInfoComponent {
  weatherResource = resource<WeatherData, string>({
    loader: async ({ abortSignal }) => {
      const response = await new Promise<Response>((resolve) => {
        setTimeout(() => {
          fetch('assets/weather.json', { signal: abortSignal }).then((r) =>
            resolve(r)
          );
        }, 1500);
      });
      if (!response.ok) {
        throw new Error('Could not fetch data');
      }
      const data = await response.json();
      return data as WeatherData;
    },
  });
}
Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching — figure 1

What’s happening:

  1. Imports: The Component and resource symbols are brought in from @angular/core.
  2. Types: The WeatherData interface mirrors the expected shape of the JSON payload.
  3. Component Properties:
    • weatherResource: A Resource instance that oversees the async data operation. The outcome of calling resource() is stored here.
  4. Setting Up weatherResource:
    • The resource() function initializes weatherResource during component construction. This action dispatches the initial HTTP request via the loader.
    • The loader is specified as an async function. It mimics a network call by reading from assets/weather.json after a 1.5-second pause using setTimeout, and accepts an abortSignal for cancellation support.
    • If the network response indicates failure, an error is thrown, which the resource() method captures.
    • On success, the loaded data is returned.
  5. Template:
    • A button invokes the weatherResource.reload() method. This action re-executes the loader and refreshes the data.
    • The markup relies on @if blocks and the resource’s isLoading() and value() methods to conditionally render either a spinner or the weather report.

Essential points:

  • The resource() API streamlines async operations by monitoring loading status, capturing errors, and exposing the result once completed.
  • Reactivity is built in; the template updates automatically as the resource’s state evolves.
  • We use weatherResource.reload() to trigger a fresh data retrieval.

Part 2: Deferred Resource Loading

In the previous example, the resource started fetching right away upon component creation. But what if this initial request should be postponed until a user action occurs? By employing the request option, you can dictate when the resource’s loader fires, thereby enabling delayed or conditional loading.

Let’s adapt the prior example by adding a weatherRequestState signal. This signal acts as a gatekeeper, deciding whether the loader should run. As a result, resource loading happens only in response to user-triggered events.

Here’s the revised code:

import { Component, resource, signal } from '@angular/core';

interface WeatherData {
  temperature: number;
  condition: string;
  icon: string;
}

type WeatherRequestState = 'idle' | 'ready';

@Component({
  selector: 'app-weather-info',
  template: `
    <div class="card bg-base-200 w-96 shadow-xl mx-auto">
      <div class="card-body flex flex-col items-center gap-4">
        <button class="btn btn-block btn-primary btn-outline" (click)="getWeatherInfo()">
          Fetch Weather
        </button>
        @if (weatherResource.isLoading()) {
          <span class="loading loading-spinner loading-lg"></span>
        } @else if (weatherResource.value()) {
          <img
            [src]="weatherResource.value()?.icon"
            class="w-20 object-fit"
            alt="weather icon"
          />
          <p class="text-2xl">
            Temperature: {{ weatherResource.value()?.temperature }}
          </p>
          <p class="text-xl">
            Condition: {{ weatherResource.value()?.condition }}
          </p>
        }
      </div>
    </div>
  `,
})
export class WeatherInfoComponent {
  weatherRequestState = signal<WeatherRequestState>('idle');
  weatherResource = resource<WeatherData, WeatherRequestState | undefined>({
    request: () => {
      if (this.weatherRequestState() === 'idle') {
        return undefined;
      }
      return this.weatherRequestState();
    },
    loader: async ({ abortSignal }) => {
      const response = await new Promise<Response>((resolve) => {
        setTimeout(() => {
          fetch('assets/weather.json', { signal: abortSignal }).then((r) =>
            resolve(r)
          );
        }, 1500);
      });
      if (!response.ok) {
        throw new Error('Could not fetch data');
      }
      const data = await response.json();
      return data as WeatherData;
    },
  });

  getWeatherInfo() {
    if (this.weatherRequestState() !== 'ready') {
      this.weatherRequestState.set('ready');
    } else {
      this.weatherResource.reload();
    }
  }
}
Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching — figure 2

Modifications:

  • A weatherRequestState signal now regulates loading. Its permitted values are 'idle' or 'ready'.
  • The resource’s request is defined as a function that returns the current signal value, or undefined when the state is 'idle'.
  • The loader executes solely when the request function yields 'ready'.
  • The getWeatherInfo() routine verifies the state: if it’s 'idle', it flips the signal to 'ready'; otherwise, it invokes weatherResource.reload() for a refresh.

Essential points:

  • The request attribute enables on-demand loading, ensuring the resource is fetched only when truly necessary.
  • Loader execution is contingent on the request function returning a non-undefined value.

Part 3: Graceful Error Management

Network requests can fail, so handling errors gracefully is critical. Let’s enhance the application to incorporate error handling and simulate a failed data request.

A new button will be added to trigger a request that intentionally ends in an error. This will showcase how to surface error messages to the user.

The code is updated as follows:

import { Component, resource, signal } from '@angular/core';
interface WeatherData {
  temperature: number;
  condition: string;
  icon: string;
}

type WeatherRequestState = 'idle' | 'ready' | 'simulateError';

@Component({
  selector: 'app-weather-info',
  template: `
    <div class="card bg-base-200 w-96 shadow-xl mx-auto">
      <div class="card-body flex flex-col items-center gap-4">
        <button class="btn btn-block btn-primary btn-outline" (click)="getWeatherInfo()">
          Fetch Weather
        </button>
        <button
          class="btn btn-block btn-error btn-outline"
          (click)="getWeatherInfoWithError()"
        >
          Get Weather Info with error
        </button>
        @if (weatherResource.isLoading()) {
          <span class="loading loading-spinner loading-lg"></span>
        } @else if (weatherResource.error()) {
          <div role="alert" class="alert alert-error">
            <svg>...</svg>
            <span>{{ weatherResource.error() }}</span>
          </div>
        } @else if (weatherResource.value()) {
          <img
            [src]="weatherResource.value()?.icon"
            class="w-20 object-fit"
            alt="weather icon"
          />
          <p class="text-2xl">
            Temperature: {{ weatherResource.value()?.temperature }}
          </p>
          <p class="text-xl">
            Condition: {{ weatherResource.value()?.condition }}
          </p>
        }
      </div>
    </div>
  `,
})
export class WeatherInfoComponent {
  weatherRequestState = signal<WeatherRequestState>('idle');
  weatherResource = resource<WeatherData, WeatherRequestState | undefined>({
    request: () => {
      if (this.weatherRequestState() === 'idle') {
        return undefined;
      }
      return this.weatherRequestState();
    },
    loader: async ({ abortSignal, request: requestState }) => {
      const response = await new Promise<Response>((resolve) => {
        setTimeout(() => {
          fetch('assets/weather.json', { signal: abortSignal }).then((r) =>
            resolve(r)
          );
        }, 1500);
      });
      if (!response.ok) {
        throw new Error('Could not fetch data');
      }
      if (requestState === 'simulateError') {
        throw new Error('Something went wrong');
      }
      const data = await response.json();
      return data as WeatherData;
    },
  });

  getWeatherInfo() {
    if (this.weatherRequestState() !== 'ready') {
      this.weatherRequestState.set('ready');
    } else {
      this.weatherResource.reload();
    }
  }

  getWeatherInfoWithError() {
    this.weatherRequestState.set('simulateError');
  }
}
Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching — figure 3

Modifications:

  • The WeatherRequestState type now includes a simulateError value.
  • The weatherRequestState signal is used to indicate when the loader should fake an error.
  • Within the loader, a check is performed: if the request state equals 'simulateError', a new error is thrown.
  • A dedicated getWeatherInfoWithError method is introduced; it assigns simulateError to the signal, which in turn triggers a load that fails.
  • The template now renders any error using weatherResource.error().

Essential points:

  • The weatherRequestState signal governs both the initial load initiation and the simulation of loader errors.
  • Errors thrown inside the loader are caught by resource() and exposed through the error() signal.
  • Displaying the error message in the template is achieved via weatherResource.error().

Part 4: Adaptive Resource Selection (Multi-City Scenario)

Now we’ll introduce more dynamic behavior by letting users switch between single and multi-city modes. When the mode changes, we’ll alter how the resource gathers data, leveraging the request option to steer the fetching process.

A toggle control and a dropdown menu will be implemented, giving users the ability to pick a city when operating in multi-city mode.

The full component code is shown below:

import { Component, resource, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

type City = 'Stockholm' | 'Milan';

interface WeatherData {
  temperature: number;
  condition: string;
  icon: string;
  city?: City;
}

type WeatherRequestState = 'idle' | 'ready' | 'simulateError';

type WeatherResourceConfig = {
  requestState: WeatherRequestState;
  isMultiCityMode: boolean;
  selectedCity: City;
};

@Component({
  selector: 'app-weather-info',
  imports: [FormsModule],
  template: `
    <div class="card bg-base-200 w-96 shadow-xl mx-auto">
      <div class="card-body flex flex-col items-center gap-4">
        <div class="form-control w-full">
          <label class="label cursor-pointer">
            <span class="label-text">Multicity</span>
            <input
              (ngModelChange)="getWeatherInfo()"
              [(ngModel)]="isMultiCityMode"
              type="checkbox"
              class="toggle toggle-primary"
            />
          </label>
        </div>
        @if (isMultiCityMode()) {
          <select [(ngModel)]="selectedCity" class="select select-primary w-full">
            @for(city of cities; track city) {
            <option [value]="city">{{ city }}</option>
            }
          </select>
        } @else {
          <button
            class="btn btn-block btn-primary btn-outline"
            (click)="getWeatherInfo()"
          >
            Get Weather Info
          </button>
        }
        <button
          class="btn btn-block btn-error btn-outline"
          (click)="getWeatherInfoWithError()"
        >
          Get Weather Info with error
        </button>
        @if (weatherResource.isLoading()) {
          <span class="loading loading-spinner loading-lg"></span>
        } @else if (weatherResource.error()) {
          <div role="alert" class="alert alert-error">
            <svg>...</svg>
            <span>{{ weatherResource.error() }}</span>
          </div>
        } @else if (weatherResource.value()) {
          <img
            [src]="weatherResource.value()?.icon"
            class="w-20 object-fit"
            alt="weather icon"
          />
          <p class="text-2xl">
            Temperature: {{ weatherResource.value()?.temperature }}
          </p>
          <p class="text-xl">
            Condition: {{ weatherResource.value()?.condition }}
          </p>
        }
      </div>
    </div>
  `,
})
export class WeatherInfoComponent {
  weatherRequestState = signal<WeatherRequestState>('idle');
  isMultiCityMode = signal<boolean>(false);
  cities: City[] = ['Stockholm', 'Milan'];
  selectedCity = signal<City>(this.cities[0]);
  weatherResource = resource<
    WeatherData | undefined,
    WeatherResourceConfig | undefined
  >({
    request: () => {
      if (this.weatherRequestState() === 'idle') {
        return undefined;
      }
      return {
        requestState: this.weatherRequestState(),
        isMultiCityMode: this.isMultiCityMode(),
        selectedCity: this.selectedCity(),
      };
    },
    loader: async ({ abortSignal, request }) => {
      if (!request) {
        return undefined;
      }
      const { requestState, isMultiCityMode, selectedCity } = request;
      const response = await new Promise<Response>((resolve) => {
        setTimeout(() => {
          const url = isMultiCityMode
            ? 'assets/weather-multi.json'
            : 'assets/weather.json';
          fetch(url, { signal: abortSignal }).then((r) => resolve(r));
        }, 1500);
      });
      if (!response.ok) {
        throw new Error('Could not fetch data');
      }
      if (requestState === 'simulateError') {
        throw new Error('Something went wrong');
      }
      const data = await response.json();
      if (isMultiCityMode) {
        const weatherInfo = (data as WeatherData[]).find(
          (info) => info.city === selectedCity
        );
        if (!weatherInfo) {
          throw new Error('Weather info not found');
        }
        return weatherInfo;
      }
      return data as WeatherData;
    },
  });

  getWeatherInfo() {
    if (this.weatherRequestState() !== 'ready') {
      this.weatherRequestState.set('ready');
    } else {
      this.weatherResource.reload();
    }
  }

  getWeatherInfoWithError() {
    this.weatherRequestState.set('simulateError');
  }
}
Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching — figure 4

Modifications:

  • FormsModule: This is imported to enable ngModel two-way binding on the toggle and the select element.
  • cities and selectedCity: A static array of cities is defined, along with a selectedCity signal to track the user’s choice.
  • isMultiCityMode: A signal that records whether the multi-city feature is active.
  • The request option is expanded to pass weatherRequestState, isMultiCityMode, and selectedCity into the loader.
  • Template:
    • A toggle is provided to flip between single and multi-city modes.
    • When multi-city mode is active, a select dropdown appears, enabling city selection.
  • getWeatherInfo Method: This function checks the current mode and proceeds to set weatherRequestState to ready or invoke a reload accordingly.
  • getWeatherInfoWithError() Method: Assigns simulateError to the signal, causing a load that results in an error while in single mode.
  • The loader now pulls different datasets depending on isMultiCityMode. For multi-city, it retrieves data from weather-multi.json and filters entries by the selected city.

Essential points:

  • Data loading is now directed through the weatherRequestState signal, enabling on-demand resource initialization.
  • The request attribute serves as a channel to transmit additional context to the loader.
  • With the value from request, the loader can manage various scenarios: standard loading, errors, single-city, and multi-city behavior.

Final Thoughts

Throughout this step-by-step walkthrough, we’ve demonstrated how to apply Angular’s resource() method for data retrieval, error handling, and dynamic switching between fetch approaches. Starting with simple examples, we’ve progressively covered complex cases and shown how to exert fine-grained control over when resources load. We’ve also examined how the request attribute can relay extra information to the loader, enhancing the adaptability of our data operations.

Important Consideration: The provided examples are intended solely for demonstrating the resource API’s capabilities. In real-world applications, it’s advisable to encapsulate HTTP calls within dedicated Angular services and implement error management in a more comprehensive, centralized fashion. Moreover, avoid embedding intentional error simulations directly in component code.

Note: promotional links are listed below...

I trust this guide proves beneficial to your Angular progress.
Happy coding!


Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching — figure 5