@ngx-data/query
v0.0.2
Published
> Asynchronous state management solution for Angular 15, built on RxJS and inspired by Tanstack's `react-query`.
Downloads
6
Readme
Ngx-data
Asynchronous state management solution for Angular 15, built on RxJS and inspired by Tanstack's
react-query
.
The goal
Interacting with data sources asynchronously is a key activity for Angular and Angular universal applications. While Angular comes with an amazing defacto solutions (RxJS) for fetching data and building declarative state management capabilities they also bring steep learning curves to new comers due to its declarative, push centric design paradigm and its huge collection of powerful yet quirky operators. Even as a seasoned user of RxJS, the need to copy/paste declarative patterns to new features and deal with differences between legacy patterns and shiny new ones I just came up with made me groan. This is why I made @ngx-data
, a collection of utilities and injectables that will hopefully make working with data easier for you and me in the future.
Thank you so much for checking out this library, if you have any suggestions and comments feel free to open an issue I will respond as soon as I can :)
The design
After looking at serveral asynchronous state management solutions and strategies, the following design principles are solidified:
- The client must be easy to adopt.
- The client should be used in the fascade/service layer.
- The client should offer necesary capabilities even if there are performance tradeoffs.
Installation and your first query
To add this package to your project, install it with the package manager of your choice.
npm install @ngx-data/query
yarn @ngx-data/query
Then import the query client module (NgxDataQueryModule
) in the app.module.ts
, this will provide the query client to the rest of your application.
@NgModule({
imports: [BrowserModule, HttpClientModule, NgxDataQueryModule.forRoot()],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}
Now in your service/component inject the query client and start using it:
const USERS_URL = '/api/users';
@Injectable()
export class UserService {
constructor(private client: QueryClient, private http: HttpClient) {}
listUsers(): Observable<User[]> {
return this.client.query(USERS_URL, () => this.http.get<User[]>(USERS_URL));
}
}
That's it! Your listUsers
method now has caching enabled!
Mutating data
When you change the data - add a new record, update an existing one, or delete one; you want the data to be refetched, you can use the invalidate
feature to invalidate the cached data and trigger a refetch.
const USERS_URL = '/api/users';
@Injectable()
export class UserService {
constructor(private client: QueryClient, private http: HttpClient) {}
listUsers(): Observable<User[]> {
return this.client.query(USERS_URL, () => this.http.get<User[]>(USERS_URL));
}
updateUser(id: number, data: User) {
return this.http
.put(`${USERS_URL}/${id}`, data)
.pipe(tap(() => this.client.invalidate(USERS_URL)));
}
}
Recreating a query
To recreate a query if it already exists, use the forceRecreate
option when calling .query
.
this.client.query(['todo'], {
forceRecreate: true,
});
When you recreate a query the existing query will be discharged emiting an EMPTY
complete response to all subscriptions and completing the original observable. All original subscribers will no longer have access to data updates. Be careful when you recreate queries.
Setting automatic retries
The query client will retry your observable if it produces errors, you may change this behavior on a per request basis or globally by setting the retries
option. Default value is 3 times
this.client.query(['todo'], {
retries: 0, // Do not retry
});
this.client.query(['todo'], {
retries: Infinity, // Never stop retrying
});
Setting a query expiry time
The query client can automatically refetch data periodically using cache expiration time. Set it using the query config. The unit is miliseconds and the default value is Infinity.
this.client.query(['todo'], {
expiresIn: 60_000, // Refetch the data automatically every minute
});
When a request expires it will automatically refetch the data from the provided observable.
Observing request status
You can observe the status on an observable using the query client's getQueryStatus
method. You can use the query status to determine the state of the query. The available states are:
idle
- when a query has been created but no data has been cached or fetchedloading
- when a query's observable upstream is being resolvedsuccess
- when a query has fetched data successfully and has the data cached in memoryerror
- when a query failed all retry attempts, and an error was thrown every timestale
- when a query has passed itsstalesIn
timer.
To get an observable stream of the request state, do:
class Component {
status$ = this.client.getQueryStatus(['todo', { done: true }]);
}
Then in your template
<ng-container *ngIf="status$ as status">
<div [ngSwitch]="status" *ngIf="status !== success">
<div *ngSwitchCase="'loading'">Loading</div>
<div *ngSwitchCase="'stale'">The data is stale</div>
<div *ngSwitchCase="'error'">There is an error</div>
</div>
</ng-container>
Template utilities
lib-data-loader
This abstraction simplifies the process of loading an observable in template. Access it by importing NgxDataLoaderModule
Then add the component inside your template and pass an observable via dataSource
input.
<ngx-data-loader [dataSource]="data$">
<ng-template #content let-data>
<span class="data">{{ data }}</span>
</ng-template>
<ng-template #loading>
<span class="loading">Loading...</span>
</ng-template>
<ng-template #error let-error>
<span class="error">{{ error }}</span>
</ng-template>
</ngx-data-loader>
Use a template with the selector #content
to define the elements to render when data is loaded, you can access the results of the observable by using a template let-X
variable, X can be anything as the data is provided through $implicit
context
Use the #loading
and #error
templates to display loading screen and error information respectively. Error data can be accessed via template context as well.
Gotcha: the loading template is only shown during the initial loading of the observable, if only the [dataSource]
observable is provided.