0.0.2 • Published 1 year ago

@ngx-data/query v0.0.2

Weekly downloads
-
License
-
Repository
-
Last release
1 year ago

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:

  1. The client must be easy to adopt.
  2. The client should be used in the fascade/service layer.
  3. 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 fetched
  • loading - when a query's observable upstream is being resolved
  • success - when a query has fetched data successfully and has the data cached in memory
  • error - when a query failed all retry attempts, and an error was thrown every time
  • stale - when a query has passed its stalesIn 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.

0.0.2

1 year ago

0.0.1

1 year ago