2

I have app with rxjs v7.5, ngrx v14.0 and use localStorage to cache http responses.
When I get data straight from http everything is working fine, but when I returning data from storage by rxjs operator: of(value) and that doesn't trigger UI change detection.
My quick fix is to delay of() observable: of(value).pipe(delay(1)), but i want to return Observable properly.

local-cache.service.ts

interface CacheStorageRecord {
  expires: Date;
  value: any;
}

@Injectable({ providedIn: 'root' })
export class LocalCacheService {

  defaultExpires: number = 86400;

  constructor(private localStorage: LocalStorageService) { }

  public observable<T>(key: string, observable: Observable<T>, expires: number = this.defaultExpires): Observable<T> {
    return this.localStorage.getItem<CacheStorageRecord>(key).pipe(
      map((val: CacheStorageRecord) => val && (new Date(val.expires)).getTime() > Date.now() ? val : null),
      switchMap((val: CacheStorageRecord | null) => {
        // return value from cache if exists or is not expired
        // if (val) return of(val.value); <-- this doesn't trigger change detection

        if (val) return of(val.value).pipe(delay(1)); // <-- it working

        // return value straight from http api
        return observable.pipe(
          switchMap((val: any) => this.value(key, val, expires))
        );
      })
    );
  }
  value<T>(key: string, value: T, expires: number | string | Date = this.defaultExpires): Observable<T> {
    let _expires: Date = this.sanitizeAndGenerateDateExpiry(expires);
    console.warn('value', value);
    return this.localStorage
      .setItem<CacheStorageRecord>(key, { expires: _expires, value: value })
      .pipe(map((item) => item.value));
  }
  // rest of code
}

planet.service.ts

@Injectable()
export class PlanetService {

  constructor(private apiHttpService: ApiHttpService, private apiEndpointsService: ApiEndpointsService, private cache: LocalCacheService) {}

  getPlanetsResponse(page: number): Observable<PlanetsResponseDTO> {
    const httpGet$: Observable<PlanetsResponseDTO> =
      this.apiHttpService.get<PlanetsResponseDTO>(this.apiEndpointsService.getPlanetsResponseEndpoint(page));

    return this.cache.observable(`planets?page=${page}`, httpGet$, 300);
  }
}

planet.effects.ts

@Injectable()
export class PlanetEffects {
  constructor(private actions$: Actions,
              private planetService: PlanetService
  ) {}

  loadPlanets$ = createEffect(() =>
    this.actions$.pipe(
      ofType(getPlanets),
      switchMap((action) =>
        from(this.planetService.getPlanetsResponse(action.page)).pipe(
          map((planetsResponse) => {
            console.log(planetsResponse)
            return getPlanetsSuccess({ planetsResponse: planetsResponse })
          }),
          catchError((error) => of(getPlanetsFailure({ payload: error })))
        )
      )
    )
  )
}

planet.sandbox.service.ts

@Injectable()
export class PlanetSandboxService {
  planets$: Observable<PlanetDTO[]>;
  page$: Observable<Page> = this.store.select(selectPage);
  pageNumber$: Subject<number> = new ReplaySubject<number>();

  constructor(private store: Store) {
    const pageNumberChanged$ = combineLatest([this.pageNumber$, this.page$]).pipe(
      filter(([pageNumber, page]) => pageNumber !== null && (!page || page.currentPage !== pageNumber)),
      map(([pageNumber, page]) => pageNumber ? pageNumber : 1),
      distinctUntilChanged()
    )

    this.planets$ = pageNumberChanged$.pipe(
      tap(page => {
        this.store.dispatch(getPlanets({ page: page }))
      }),
      switchMap(() => this.store.select(selectPlanets)),
    );
  }
}

0 Answers0