https://github.com/odonno/ngrx-lazy
Dead simple data lazy loading for @ngrx state management. Inspired by the early work of @Synthx
https://github.com/odonno/ngrx-lazy
angular lazy-loading ngrx rxjs state state-management
Last synced: 2 months ago
JSON representation
Dead simple data lazy loading for @ngrx state management. Inspired by the early work of @Synthx
- Host: GitHub
- URL: https://github.com/odonno/ngrx-lazy
- Owner: Odonno
- Created: 2022-08-22T12:39:50.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2023-02-05T17:35:33.000Z (over 3 years ago)
- Last Synced: 2025-05-25T03:37:29.154Z (about 1 year ago)
- Topics: angular, lazy-loading, ngrx, rxjs, state, state-management
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/ngrx-lazy
- Size: 458 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ngrx-lazy
Dead simple data lazy loading for [@ngrx](https://github.com/ngrx) state management.
Inspired by the early work of [@Synthx](https://github.com/Synthx)
## Get started
```
npm install ngrx-lazy --save
```
## How to use?
To fully understand how to use this library, we'll go through an example of a project that list brands on an e-commerce website.
### Lazy state
A lazy state is made of several properties:
```ts
type Lazy = {
fetched: boolean;
pending: boolean;
error?: any;
data: T;
};
```
You can then use the `createLazy` function to initialize your feature state.
```ts
import { createLazy, Lazy } from "ngrx-lazy";
import { Brand } from "./models";
export interface BrandsState {
all: Lazy;
popular: Lazy;
}
export const initialBrandsState: BrandsState = {
all: createLazy([]),
popular: createLazy([]),
};
```
In this example, we create two lazy loadable array of brands inside our brands feature state.
### Lazy reducers
Once you have created your state, you can handle the reducers based on a simple data fetching logic.
```ts
import { createReducer, on } from "@ngrx/store";
import * as BrandsActions from "./brands.actions";
import { initialBrandsState } from "./brands.state";
export const brandsReducer = createReducer(
initialBrandsState,
on(BrandsActions.loadAll, (state) => ({
...state,
all: { ...state.all, pending: true },
})),
on(BrandsActions.loadAllSuccess, (state, { brands }) => ({
...state,
all: { data: brands, fetched: true, pending: false },
})),
on(BrandsActions.loadAllError, (state, { error }) => ({
...state,
all: { ...state.all, pending: false, error },
})),
on(BrandsActions.loadPopular, (state) => ({
...state,
popular: { ...state.popular, pending: true },
})),
on(BrandsActions.loadPopularSuccess, (state, { popularBrands }) => ({
...state,
popular: { data: popularBrands, fetched: true, pending: false },
})),
on(BrandsActions.loadPopularError, (state, { error }) => ({
...state,
popular: { ...state.popular, pending: false, error },
}))
);
```
### Lazy selectors
You can create basic selectors using the existing `createSelector` from @ngrx.
```ts
import { createSelector } from "@ngrx/store";
import { getEntityState } from "../selectors/entity.selectors";
const selectBrandsState = createSelector(
getEntityState,
(state) => state.brands
);
const lazyAll = createSelector(selectBrandsState, (state) => state.all);
const selectBrands = createSelector(lazyAll, (lazy) => lazy.data);
const selectBrandBySlug = (slug: string) =>
createSelector(selectBrands, (brands) => brands.find((b) => b.slug === slug));
export const AllBrandsSelectors = {
selectLazy: lazyAll,
selectBrands,
selectBrandBySlug,
};
const lazyPopular = createSelector(selectBrandsState, (state) => state.popular);
const selectPopularBrands = createSelector(lazyPopular, (lazy) => lazy.data);
export const PopularBrandsSelectors = {
selectLazy: lazyPopular,
selectPopularBrands,
};
```
In order to make this library shine, you absolutely need a Facade on top of the selector. Here is an example what it can looks like:
```ts
import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import { selectLazy } from "ngrx-lazy";
import { RootState } from "../models";
import * as BrandsActions from "./brands.actions";
import { AllBrandsSelectors, PopularBrandsSelectors } from "./brands.selectors";
@Injectable()
export class BrandsSelectors {
constructor(private readonly store: Store) {}
lazyAll$ = selectLazy({
store: this.store,
selector: AllBrandsSelectors.lazyAll,
loadAction: BrandsActions.loadAll(),
});
brands$ = this.lazyAll$.select(AllBrandsSelectors.getBrands);
brandBySlug$ = (slug: string) =>
this.lazyAll$.select(AllBrandsSelectors.getBrandBySlug(slug));
brandById$ = (id: number) =>
this.lazyAll$.select(AllBrandsSelectors.getBrandById(id));
lazyPopular$ = selectLazy({
store: this.store,
selector: PopularBrandsSelectors.lazyPopular,
loadAction: BrandsActions.loadPopular(),
});
popularBrands$ = this.lazyPopular$.select(
PopularBrandsSelectors.getPopularBrands
);
}
```
Now, when you call any property on this selector service, it will automatically trigger the data loading related to state of the lazy state.
Of course, you'd need to implement Effects to execute data fetching!
### RxJS operators
#### firstNotPending
The operator `firstNotPending` takes a lazy selector and dispatch the first value of the selector once data fetching is done.
```ts
const allBrands$: Observable = this.brandsSelectors.lazyAll$.pipe(firstNotPending()):
```
#### skipUntilLazyLoaded
The operator `skipUntilLazyLoaded` takes a selector and dispatch its values only once a parent selector is fully lazy loaded (once data fetching is done).
```ts
const brand$: Observable = this.brandsSelectors.brandById$(id).pipe(
skipUntilLazyLoaded(this.brandsSelectors.lazyAll$),
):
```