Naren Murali
Naren Murali

Reputation: 57986

How to stop listening for signal changes, but retain the last fetched value in angular resource API - PAUSE functionality for Resource API

I have been going through the start/stop of resource API and wanted to know if there is a way to achieve a pause like functionality, but that I mean:

The Resource API should stop listening for signal changes, but retain the last fetched value

When I stop the resource listener, it emptied the value inside the resource.value(), but I want to use this value in my UI and just stop listening for future signal changes which are present in the request callback of either resource or rxResource.

Below is my minimal reproducible code along with working Stackblitz, for debugging:

HTML:

<div>
  <div>
    Resource Request triggers:
  </div>
  <div>
    <input [(ngModel)]="id" type="number"/>
  </div>
</div>
<div>
  @if(![rs.Loading, rs.Reloading].includes(rxResource.status())) {
    {{rxResource.value() | json}}
  } @else{
    Loading...
  }
</div>
<hr/>
<div>
  @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
    {{resource.value() | json}}
  } @else{
    Loading...
  }
</div>
<hr/>
<div>
<div>Current Status: {{rs[resource.status()]}}</div>
<button (click)="start()">Start Listening</button>
<button (click)="pause()">Pause Listening (Retain last value)</button>
<button (click)="stop()">Stop Listening</button>
</div>

TS:

export class App {
  id = signal(1);
  rs = ResourceStatus;
  http = inject(HttpClient);
  resourceControl = signal(true);
  resourceRequest = (): ResourceRequest | undefined => {
    if (this.resourceControl()) {
      return {
        id: this.id(),
      };
    } else {
      return undefined;
    }
  };
  resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
    request: this.resourceRequest,
    loader: ({ request: { id }, abortSignal }) => {
      return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        signal: abortSignal,
      }).then((res: any) => res.json());
    },
  });
  rxResource: ResourceRef<any> = rxResource<ResourceRequest | undefined, any>({
    request: this.resourceRequest,
    loader: ({ request: { id } }) => {
      return this.http.get<any>(
        `https://jsonplaceholder.typicode.com/todos/${id}`
      );
    },
  });

  start() {
    this.resourceControl.set(true);
  }

  stop() {
    this.resourceControl.set(false);
  }

  pause() {
    // how to pause the resource and stop listening to future changes but retain the last value
  }
}

Stackblitz Demo

Upvotes: -3

Views: 47

Answers (1)

Naren Murali
Naren Murali

Reputation: 57986

I have been following the below answer:

How do I start/stop the angular resource and rxResource API which is declared at the initialization of a class or service

We can extend this approach, to integrate pause functionality also.

The base concept is to store the last used value and return it without triggering the API call, which can be easily done using effect (to sync the last fetched value) and Promise.resolve(return promise for resource) and of(return observable for rxResource) with this previous value.


First we define a control enum which determines the mode of the Resource API. We will have three modes:

  1. START -> the resource API operates normally (eagerly evaluates source signals and reacts to new changes)

  2. PAUSE -> the resource API retains the last fetched value and does not listen for future signal changes.

  3. STOP -> the resource API will not have any value and will not listen for future signal changes.

     export enum ResourceState {
       START,
       STOP,
       PAUSE,
     }
    

We then create a property which will be initialized to ResourceState.START, this property controls the start/stop/pause of the resource API.

resourceControl = signal(ResourceState.START);

Then, we setup two properties to store the last fetched value, one for resource and one for rxResource. I am demonstrating both in the same example:

prevValue = signal(undefined);
prevRxValue = signal(undefined);

We initialize an effect which can be used to store the last fetched successful value from the resource API.

constructor() {
  effect(() => {
    if (this.rxResource.status() === ResourceStatus.Resolved) {
      this.prevRxValue.set(this.rxResource.value());
    }
    if (this.resource.status() === ResourceStatus.Resolved) {
      this.prevValue.set(this.resource.value());
    }
  });
}

Pause:

We achieve pause, by checking the resource state through our loader callback and then return the previous value using Promise.resolve(return promise for resource) and of(return observable for rxResource) with this previous value:

resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
  request: this.resourceRequest,
  loader: ({ request: { id }, abortSignal }) => {
    return this.resourceControl() === ResourceState.PAUSE
      ? Promise.resolve(this.prevValue())
      : fetch(..., {
          signal: abortSignal,
        }).then((res: any) => res.json());
  },
});
rxResource: ResourceRef<any> = rxResource<ResourceRequest | undefined, any>({
  request: this.resourceRequest,
  loader: ({ request: { id } }) => {
    return this.resourceControl() === ResourceState.PAUSE
      ? of(this.prevRxValue())
      : this.http.get<any>(...);
  },
});

Start/Stop:

Using our ResourceState enum, we configure the STOP functionality by returning undefined, which stops the resource API.

resourceRequest = (): ResourceRequest | undefined => {
  if (this.resourceControl() === ResourceState.STOP) {
    return undefined;
  } else {
    return {
      id: this.id(),
    };
  }
};

We then setup buttons to toggle the respective state:

start() {
  this.resourceControl.set(ResourceState.START);
}

stop() {
  this.resourceControl.set(ResourceState.STOP);
}

pause() {
  this.resourceControl.set(ResourceState.PAUSE);
}

Full Code:

import {
  Component,
  inject,
  ResourceRef,
  ResourceStatus,
  signal,
  resource,
  computed,
  effect,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';

export interface ResourceRequest {
  id: number;
}

export enum ResourceState {
  START,
  STOP,
  PAUSE,
}

@Component({
  selector: 'app-root',
  imports: [CommonModule, FormsModule],
  template: `
  <div>
    <div>
      Resource Request triggers:
    </div>
    <div>
      <input [(ngModel)]="id" type="number"/>
    </div>
  </div>
    <div>
      @if(![rs.Loading, rs.Reloading].includes(rxResource.status())) {
        {{rxResource.value() | json}}
      } @else{
        Loading...
      }
    </div>
    <hr/>
    <div>
      @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
        {{resource.value() | json}}
      } @else{
        Loading...
      }
    </div>
    <hr/>
    <div>
    <div>Resource API State: {{rState[resourceControl()]}}</div>
    <div>Current Status: {{rs[resource.status()]}}</div>
    <button (click)="start()">Start Listening</button>
    <button (click)="pause()">Pause Listening (Retain last value)</button>
    <button (click)="stop()">Stop Listening</button>
    </div>
  `,
})
export class App {
  id = signal(1);
  rs = ResourceStatus;
  rState = ResourceState;
  http = inject(HttpClient);
  resourceControl = signal(ResourceState.START);
  resourceRequest = (): ResourceRequest | undefined => {
    if (this.resourceControl() === ResourceState.STOP) {
      return undefined;
    } else {
      return {
        id: this.id(),
      };
    }
  };
  resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
    request: this.resourceRequest,
    loader: ({ request: { id }, abortSignal }) => {
      return this.resourceControl() === ResourceState.PAUSE
        ? Promise.resolve(this.prevValue())
        : fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            signal: abortSignal,
          }).then((res: any) => res.json());
    },
  });
  rxResource: ResourceRef<any> = rxResource<ResourceRequest | undefined, any>({
    request: this.resourceRequest,
    loader: ({ request: { id } }) => {
      return this.resourceControl() === ResourceState.PAUSE
        ? of(this.prevRxValue())
        : this.http.get<any>(
            `https://jsonplaceholder.typicode.com/todos/${id}`
          );
    },
  });
  prevValue = signal(undefined);
  prevRxValue = signal(undefined);

  constructor() {
    effect(() => {
      if (this.rxResource.status() === ResourceStatus.Resolved) {
        this.prevRxValue.set(this.rxResource.value());
      }
      if (this.resource.status() === ResourceStatus.Resolved) {
        this.prevValue.set(this.resource.value());
      }
    });
  }

  start() {
    this.resourceControl.set(ResourceState.START);
  }

  stop() {
    this.resourceControl.set(ResourceState.STOP);
  }

  pause() {
    this.resourceControl.set(ResourceState.PAUSE);
  }
}

bootstrapApplication(App, {
  providers: [provideHttpClient()],
});

Stackblitz Demo

Upvotes: -1

Related Questions