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)),
);
}
}