DEV Community

Connie Leung
Connie Leung

Posted on

Provide the default value in the Resource API

The Resource API includes resource and rxResource functions whose default value is undefined. When developers only provide a loader, the return type of the functions is T | undefined. In Angular 19.2.0, the Resource API will add a defaultValue option that overrides the undefined, and the functions return type T. This default value is used when the resource is undefined, loads data, or throws an error.

In this blog post, I will show you how to use the defaultValue option to return a dummy Person object in the rxResource function.

Add defaultValue option to the Resource API

Use rxResource to retrieve Star Wars details

export function getPerson() {
 assertInInjectionContext(getPerson);
 const http = inject(HttpClient);
 return (id: number) => {
   return http.get<Person>(`https://swapi.dev/api/people/${id}`).pipe(
     delay(500),
     map((p) => ({...p, id } as Person)),
     catchError((err) => {
       console.error(err);
       return throwError(() => err);
     }));
 }
}
Enter fullscreen mode Exit fullscreen mode

The getPerson function calls the endpoint to retrieve a Star Wars character. If the HTTP request throws an error, the function catches it, logs the error message, and rethrows it. The HTTP request adds 500 milliseconds delay to simulate long resource loading.

const DEFAULT: Person = {
 id: -1,
 name: 'NA',
 height: 'NA',
 mass: 'NA',
 hair_color: 'NA',
 skin_color: 'NA',
 eye_color: 'NA',
 gender: 'NA',
 films: [],
}

export function getStarWarsCharacter(id: Signal<number>, injector: Injector) {
 return runInInjectionContext(injector, () => {
   const getPersonFn = getPerson();
   const starWarsResource = rxResource({
     request: id,
     loader: ({ request: searchId }) => getPersonFn(searchId),
     defaultValue: DEFAULT
   }).asReadonly();

   return computed(() => ({
     value: starWarsResource.value(),
     status: starWarsResource.status(),
   }));
 });
}
Enter fullscreen mode Exit fullscreen mode

The getStarWarCharacter function creates a resource that queries a Star Wars character by an id. The resource has a new defaultValue property that returns a dummy Person object when the resource is undefined, loading, or has an error. The function returns a computed signal consisting of the resource status and resource value.

Refactor the CharacterComponent

export class CharacterComponent {
 searchId2 = signal(initialId);
 injector = inject(Injector);
 person = getStarWarsCharacter(this.searchId2, this.injector);
}
Enter fullscreen mode Exit fullscreen mode

Using rxResource, I successfully query the Star Wars character from the backend server in the CharacterComponent component.

<h3>Display one of the 83 Star War Characters</h3>
<div class="border">
  <p>Resource Status: {{ person().status }}</p>
  <app-character-info [info]="person().value" />
</div>
<app-character-picker (newSearchId)="searchId2.set($event)" />
Enter fullscreen mode Exit fullscreen mode

The CharacterComponent component displays the resource status and passes the Star Wars character in the CharacterInfoComponent component. The CharacterPickerComponent component changes the ID and emits it to the parent component.

Refactor the CharacterInfoComponent

@Component({
 selector: 'app-character-info',
 standalone: true,
 template: `
   @let person = info();
   <p>Id: {{ person.id }} </p>
   <p>Name: {{ person.name }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CharacterInfoComponent {
   info = input.required<Person>();
}
Enter fullscreen mode Exit fullscreen mode

The CharacterInfoComponent is a presentation component that accepts the Star Wars character's ID and details. I moved the template logic to a new component such that the ChartacterComponent looked cleaner.

The rxResource function uses the defaultValue option to return a dummy Person object; therefore, the info signal input can never be undefined. The type of the signal input is Person and the template does not need to check info for undefined.

Refactor the CharactePickerComponent

<div class="container">
     <button (click)="delta.set({ value: -1 })">-1</button>
     <button (click)="delta.set({ value: 1 })">+1</button>
</div>
Enter fullscreen mode Exit fullscreen mode
export class CharacterPickerComponent {
   newSearchId = output<number>();
   delta = signal<{ value: number }>({ value: 0 });
   characterId = linkedSignal<{ value: number }, number>({
      source: this.delta,
      computation: ({ value }, previous) => {
         const previousValue = !previous ? initialId : previous.value;
         return previousValue + value;
      }
   });
}
Enter fullscreen mode Exit fullscreen mode

When the delta signal receives a new value, the characterId signal increments or decrements the current ID to derive the new ID.

In the constructor, the effect's callback function emits the value of the characterId LinkedSignal to the parent component. The Angular team said that the computation function of the LinkedSignal should be pure; therefore, it is not a good place to emit the value.

constructor() {
    effect(() => this.newSearchId.emit(this.characterId()));
}
Enter fullscreen mode Exit fullscreen mode

When users click the buttons to increment or decrement the ID, the default value is displayed when the rxResource function loads the data from the backend server. The user interface displays the resource status 2, the enum value of "Loading". After the data is retrieved, it is displayed, and the resource status changes to 4, the enum value of "Resolved". When users change the ID to 17, the HTTP request throws an exception. The resource status changes to 1, the enum value of "Error", and the default value is displayed.

The defaultValue option allows developers to provide the Resource API with a default value other than undefined. It is helpful when the resource must return T, not T | undefined. Extra codes are unnecessary to destructure properties and assign default values when the object is possibly null.

References:

Top comments (0)