• Stars
    star
    510
  • Rank 83,437 (Top 2 %)
  • Language
    TypeScript
  • Created over 1 year ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🚀 Powerful asynchronous state management, server-state utilities and data fetching for Angular Applications

The TanStack Query (also known as react-query) adapter for Angular applications

Get rid of granular state management, manual refetching, and async spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that improve your developer experience.

Features

✅  Backend agnostic
✅  Dedicated Devtools
✅  Auto Caching
✅  Auto Refetching
✅  Window Focus Refetching
✅  Polling/Realtime Queries
✅  Parallel Queries
✅  Dependent Queries
✅  Mutations API
✅  Automatic Garbage Collection
✅  Paginated/Cursor Queries
✅  Load-More/Infinite Scroll Queries
✅  Request Cancellation
✅  Prefetching
✅  Offline Support
✅  Data Selectors


Build Status commitizen PRs coc-badge semantic-release styled with prettier spectator

Table of Contents

Installation

npm i @ngneat/query
yarn add @ngneat/query

Queries

Query Client

Inject the QueryClientService provider to get access to the query client instance:

import { QueryClientService } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private queryClient = inject(QueryClientService);
}

Query

Inject the UseQuery in your service. Using the hook is similar to the official hook, except the query function should return an observable.

import { UseQuery } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private useQuery = inject(UseQuery);

  getTodos() {
    return this.useQuery(['todos'], () => {
      return this.http.get<Todo[]>(
        'https://jsonplaceholder.typicode.com/todos'
      );
    });
  }

  getTodo(id: number) {
    return this.useQuery(['todo', id], () => {
      return this.http.get<Todo>(
        `https://jsonplaceholder.typicode.com/todos/${id}`
      );
    });
  }
}

Use it in your component:

import { SubscribeModule } from '@ngneat/subscribe';

@Component({
  standalone: true,
  imports: [NgIf, NgForOf, SpinnerComponent, SubscribeModule],
  template: `
    <ng-container *subscribe="todos$ as todos">
      <ng-query-spinner *ngIf="todos.isLoading"></ng-query-spinner>

      <p *ngIf="todos.isError">Error...</p>

      <ul *ngIf="todos.isSuccess">
        <li *ngFor="let todo of todos.data">
          {{ todo.title }}
        </li>
      </ul>
    </ng-container>
  `,
})
export class TodosPageComponent {
  todos$ = inject(TodosService).getTodos().result$;
}

Note that using the *subscribe directive is optional. Subscriptions can be made using any method you choose.

Infinite Query

Inject the UseInfiniteQuery provider in your service. Using the hook is similar to the official hook, except the query function should return an observable.

import { UseInfiniteQuery } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class ProjectsService {
  private useInfiniteQuery = inject(UseInfiniteQuery);

  getProjects() {
    return this.useInfiniteQuery(
      ['projects'],
      ({ pageParam = 0 }) => {
        return getProjects(pageParam);
      },
      {
        getNextPageParam(projects) {
          return projects.nextId;
        },
        getPreviousPageParam(projects) {
          return projects.previousId;
        },
      }
    );
  }
}

Checkout the complete example in our playground.

Persisted Query

Use the UsePersistedQuery provider when you want to use the keepPreviousData feature. For example, to implement the pagination functionality:

import { inject, Injectable } from '@angular/core';
import {
  UsePersistedQuery,
  QueryClientService,
  queryOptions,
} from '@ngneat/query';
import { firstValueFrom } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class PaginationService {
  private queryClient = inject(QueryClientService);

  getProjects = inject(UsePersistedQuery)((queryKey: ['projects', number]) => {
    return queryOptions({
      queryKey,
      queryFn: ({ queryKey }) => {
        return fetchProjects(queryKey[1]);
      },
    });
  });

  prefetch(page: number) {
    return this.queryClient.prefetchQuery(['projects', page], () =>
      firstValueFrom(fetchProjects(page))
    );
  }
}

Checkout the complete example in our playground.

Mutations

Mutation Result

The official mutation function can be a little verbose. Generally, you can use the following in-house simplified implementation.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClientService } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private queryClient = inject(QueryClientService);

  addTodo({ title }: { title: string }) {
    return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
      tap((newTodo) => {
        // Invalidate to refetch
        this.queryClient.invalidateQueries(['todos']);
        // Or update manually
        this.queryClient.setQueryData<TodosResponse>(
          ['todos'],
          addEntity('todos', newTodo)
        );
      })
    );
  }
}

And in the component:

import { QueryClientService, useMutationResult } from '@ngneat/query';

@Component({
  template: `
    <input #ref />

    <button
      (click)="addTodo({ title: ref.value })"
      *subscribe="addTodoMutation.result$ as addTodoMutation"
    >
      Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
    </button>
  `,
})
export class TodosPageComponent {
  private todosService = inject(TodosService);
  addTodoMutation = useMutationResult();

  addTodo({ title }) {
    this.todosService
      .addTodo({ title })
      .pipe(this.addTodoMutation.track())
      .subscribe();
  }
}

Mutation

You can use the original mutation functionality if you prefer.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClientService, UseMutation } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private queryClient = inject(QueryClientService);
  private useMutation = inject(UseMutation);

  addTodo() {
    return this.useMutation(({ title }: { title: string }) => {
      return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
        tap((newTodo) => {
          // Invalidate to refetch
          this.queryClient.invalidateQueries(['todos']);
          // Or update manually
          this.queryClient.setQueryData<TodosResponse>(
            ['todos'],
            addEntity('todos', newTodo)
          );
        })
      );
    });
  }
}

And in the component:

@Component({
  template: `
    <input #ref />

    <button
      (click)="addTodo({ title: ref.value })"
      *subscribe="addTodoMutation.result$ as addTodoMutation"
    >
      Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
    </button>
  `,
})
export class TodosPageComponent {
  private todosService = inject(TodosService);
  addTodoMutation = this.todosService.addTodo();

  addTodo({ title }) {
    this.addTodoMutation$.mutate({ title }).then((res) => {
      console.log(res.success);
    });
  }
}

Query Global Options

You can provide the QUERY_CLIENT_OPTIONS provider to set the global options of the query client instance:

import { provideQueryClientOptions } from '@ngneat/query';

{
  providers: [
    provideQueryClientOptions({
      defaultOptions: {
        queries: {
          staleTime: 3000,
        },
      },
    }),
  ];
}

Operators

import {
  filterError,
  filterSuccess,
  selectResult,
  mapResultData,
  mapResultsData,
  tapSuccess,
  tapError,
  startWithQueryResult,
} from '@ngneat/query';

export class TodosPageComponent {
  public todos: Array<Todo>;
  todosService = inject(TodosService);

  ngOnInit() {
    this.todosService.getTodos().result$.pipe(filterError());
    this.todosService.getTodos().result$.pipe(filterSuccess());
    this.todosService
      .getTodos()
      .result$.pipe(selectResult((result) => result.data.foo));

    // Map the result `data`
    this.todosService.getTodos().pipe(
      mapResultData((data) => {
        return {
          todos: data.todos.filter(predicate),
        };
      })
    );

    // process error or success result directly
    this.todosService
      .getTodos()
      .result$.pipe(
        tapSuccess((data) => {
          // get result data directly if success
          this.todos = data;
        }),
        tapError((error) => {
          // do what you want with error (display modal, toast, etc)
          alert(error.message);
        })
      )
      .subscribe();
  }
}

Utils

Implementation of isFetching and isMutating.

import {
  UseIsFetching,
  UseIsMutating,
  createSyncObserverResult
} from '@ngneat/query';

// How many queries are fetching?
const isFetching$ = inject(UseIsFetching)();
// How many queries matching the posts prefix are fetching?
const isFetchingPosts$ = inject(UseIsFetching)(['posts']);

// How many mutations are fetching?
const isMutating$ = inject(UseIsMutating)();
// How many mutations matching the posts prefix are fetching?
const isMutatingPosts$ = inject(UseIsMutating)(['posts']);

// Create sync successful observer in case we want to work with one interface
of(createSyncObserverResult(data, options?))

Use Constructor DI

You can use the constructor version instead of inject:

QueryService.use(...)
PersistedQueryService.use(...)
InfiniteQueryService.use(...)
MutationService.use(...)

Devtools

Install the @ngneat/query-devtools package. Lazy load and use it only in development environment:

import { ENVIRONMENT_INITIALIZER } from '@angular/core';
import { environment } from './environments/environment';

import { QueryClientService } from '@ngneat/query';

bootstrapApplication(AppComponent, {
  providers: [
    environment.production
      ? []
      : {
          provide: ENVIRONMENT_INITIALIZER,
          multi: true,
          useValue() {
            const queryClient = inject(QueryClientService);
            import('@ngneat/query-devtools').then((m) => {
              m.ngQueryDevtools({ queryClient });
            });
          },
        },
  ],
});

SSR

On the Server:

import { provideQueryClient } from '@ngneat/query';
import { QueryClient, dehydrate } from '@tanstack/query-core';
import { renderApplication } from '@angular/platform-server';

async function handleRequest(req, res) {
  const queryClient = new QueryClient();
  let html = await renderApplication(AppComponent, {
    providers: [provideQueryClient(queryClient)],
  });
  const queryState = JSON.stringify(dehydrate(queryClient));
  html = html.replace(
    '</body>',
    `<script>window.__QUERY_STATE__ = ${queryState}</script></body>`
  );

  res.send(html);
  queryClient.clear();
}

Client:

import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import { provideQueryClient } from '@ngneat/query';
import { QueryClient, hydrate } from '@tanstack/query-core';

const queryClient = new QueryClient();
const dehydratedState = JSON.parse(window.__QUERY_STATE__);
hydrate(queryClient, dehydratedState);

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      BrowserModule.withServerTransition({ appId: 'server-app' })
    ),
    provideQueryClient(queryClient),
  ],
});

Created By

Netanel Basal
Netanel Basal

Contributors ✨

Thank goes to all these wonderful people who contributed ❤️

More Repositories

1

falso

All the Fake Data for All Your Real Needs 🙂
TypeScript
3,098
star
2

spectator

🦊 🚀 A Powerful Tool to Simplify Your Angular Tests
TypeScript
2,029
star
3

transloco

🚀 😍 The internationalization (i18n) library for Angular
TypeScript
1,856
star
4

until-destroy

🦊 RxJS operator that unsubscribe from observables on destroy
TypeScript
1,712
star
5

elf

🧙‍♀️ A Reactive Store with Magical Powers
TypeScript
1,527
star
6

content-loader

⚪️ SVG component to create placeholder loading, like Facebook cards loading.
TypeScript
733
star
7

hot-toast

🍞 Smoking hot Notifications for Angular. Lightweight, customizable and beautiful by default.
TypeScript
687
star
8

cashew

🐿 A flexible and straightforward library that caches HTTP requests in Angular
TypeScript
671
star
9

reactive-forms

(Angular Reactive) Forms with Benefits 😉
TypeScript
610
star
10

tailwind

🔥 A schematic that adds Tailwind CSS to Angular applications
TypeScript
608
star
11

forms-manager

🦄 The Foundation for Proper Form Management in Angular
TypeScript
517
star
12

error-tailor

🦄 Making sure your tailor-made error solution is seamless!
TypeScript
478
star
13

helipopper

🚁 A Powerful Tooltip and Popover for Angular Applications
TypeScript
392
star
14

nx-serverless

🚀 The Ultimate Monorepo Starter for Node.js Serverless Applications
TypeScript
388
star
15

dialog

👻 A simple to use, highly customizable, and powerful modal for Angular Applications
TypeScript
371
star
16

hotkeys

🤖 A declarative library for handling hotkeys in Angular applications
TypeScript
325
star
17

edit-in-place

A flexible and unopinionated edit in place library for Angular applications
TypeScript
252
star
18

svg-icon

👻 A lightweight library that makes it easier to use SVG icons in your Angular Application
TypeScript
251
star
19

inspector

🕵️ An angular library that lets you inspect and change Angular component properties
TypeScript
218
star
20

dirty-check-forms

🐬Detect Unsaved Changes in Angular Forms
TypeScript
199
star
21

input-mask

🎭 @ngneat/input-mask is an angular library that creates an input mask
TypeScript
199
star
22

lib

🤖 Lets you focus on the stuff that matters
TypeScript
180
star
23

transloco-keys-manager

🦄 The Key to a Better Translation Experience
TypeScript
174
star
24

dag

🐠 An Angular service for managing directed acyclic graphs
TypeScript
153
star
25

bind-query-params

Sync URL Query Params with Angular Form Controls
TypeScript
147
star
26

from-event

🦊 ViewChild and FromEvent — a Match Made in Angular Heaven
TypeScript
137
star
27

overview

🤖 A collection of tools to make your Angular views more modular, scalable, and maintainable
TypeScript
104
star
28

aim

Angular Inline Module Schematics
TypeScript
97
star
29

cmdk

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk
TypeScript
91
star
30

copy-to-clipboard

✂️ Modern copy to clipboard. No Flash.
TypeScript
78
star
31

variabless

JS & CSS - A Match Made in Heaven 💎
HTML
78
star
32

loadoff

🤯 When it comes to loaders, take a load off your mind...
TypeScript
78
star
33

effects

🪄 A framework-agnostic RxJS effects implementation
TypeScript
59
star
34

avvvatars

Beautifully crafted unique avatar placeholder for your next angular project.
TypeScript
42
star
35

react-rxjs

🔌 "Plug and play" for Observables in React Apps!
TypeScript
37
star
36

subscribe

Subscription Handling Directive
TypeScript
34
star
37

elf-ng-router-store

Bindings to connect Angular router to Elf
TypeScript
24
star
38

ng-standalone-nx

TypeScript
24
star
39

lit-file-generator

🎁 A lit generator for a component, directive, and controller.
JavaScript
19
star
40

storage

TypeScript
18
star
41

material-schematics

TypeScript
3
star
42

svg-icon-demo

TypeScript
1
star