An open API service indexing awesome lists of open source software.

https://github.com/ngneat/query

🚀 Powerful asynchronous state management, server-state utilities and data fetching for Angular Applications
https://github.com/ngneat/query

async cache data fetch http pagination query stale-while-revalidate update

Last synced: 20 days ago
JSON representation

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

Awesome Lists containing this project

README

        

> 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

✅  Observable & Signal Support
✅  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
✅  SSR Support


[![@ngneat/query](https://github.com/ngneat/query/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ngneat/query/actions/workflows/ci.yml)
[![commitizen](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)]()
[![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)]()
[![coc-badge](https://img.shields.io/badge/codeof-conduct-ff69b4.svg?style=flat-square)]()
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e5079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)

## Motivation

Discover the innovative approach TanStack Query takes to state management, setting it apart from traditional methods. Learn about the motivation behind this design and explore its unique features [here](https://tanstack.com/query/v5/docs/react/overview#motivation).

## Installation

```
npm i @ngneat/query
```

[Stackblitz Example](https://stackblitz.com/edit/stackblitz-starters-bsrgez?file=src%2Fmain.ts)

>Please be aware that the `@tanstack/query-core` package must also be installed for the functionality to operate correctly.

## Query Client

Inject the `QueryClient` [instance](https://tanstack.com/query/v5/docs/reference/QueryClient) through the `injectQueryClient()`
function.

```ts
import { injectQueryClient } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
#queryClient = injectQueryClient();
}
```
or provide `QueryClient` [instance](https://tanstack.com/query/v5/docs/reference/QueryClient) manually

```ts
import { provideQueryClient } from '@ngneat/query';
import { QueryClient } from '@tanstack/query-core';

provideQueryClient(() => new QueryClient())
```

and then use with

```ts
import { injectQueryClient } from '@ngneat/query';

...
#queryClient = injectQueryClient();
```

> Functions should run inside an injection context

### Query

Use the `injectQuery` function. Using this function is similar to the [official](https://tanstack.com/query/v5/docs/guides/queries) function.

```ts
import { injectQuery } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
#http = inject(HttpClient);
#query = injectQuery();

getTodos() {
return this.#query({
queryKey: ['todos'] as const,
queryFn: () => {
return this.#http.get(
'https://jsonplaceholder.typicode.com/todos',
);
},
});
}
}
```

> The function should run inside an injection context

For methods that require a `queryFn` parameter like
`ensureQueryData`, `fetchQuery`, `prefetchQuery`, `fetchInfiniteQuery` and `prefetchInfiniteQuery` it's possible to use both Promises and Observables. See an example [here](https://github.com/ngneat/query/blob/main/src/app/prefetch-page/resolve.ts#L9).

#### Component Usage - Observable

To get an observable use the `result$` property:

```ts
@Component({
standalone: true,
template: `
@if (todos.result$ | async; as result) {
@if (result.isLoading) {

Loading


}
@if (result.isSuccess) {

{{ result.data[0].title }}


}
@if (result.isError) {

Error


}
}
`,
})
export class TodosPageComponent {
todos = inject(TodosService).getTodos();
}
```

#### Component Usage - Signal

To get a signal use the `result` property:

```ts
@Component({
standalone: true,
template: `
@if (todos().isLoading) {
Loading
}
@if (todos().data; as data) {

{{ data[0].title }}


}
@if (todos().isError) {

Error


}
`,
})
export class TodosPageComponent {
todos = inject(TodosService).getTodos().result;
}
```

## Typing Query Options

If you inline query options into `query`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `query` and e.g. `prefetchQuery`. In that case, you'd lose type inference. To get it back, you can use `queryOptions` helper:

```ts
import { queryOptions } from '@ngneat/query';

function groupOptions() {
return queryOptions({
queryKey: ['groups'] as const,
queryFn: fetchGroups,
staleTime: 5 * 1000,
});
}
```

Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well:

```ts
@Injectable({ providedIn: 'root' })
export class GroupsService {
#client = injectQueryClient();
#http = inject(HttpClient);

groupOptions = queryOptions({
queryKey: ['groups'] as const,
queryFn: () => this.#http.get(url),
staleTime: 5 * 1000,
});

getCachedGroup() {
const data = this.#client.getQueryData(this.groupOptions.queryKey);
// ^? const data: Group[] | undefined
return data;
}
}
```

### Infinite Query

Use the `injectInfiniteQuery` function. Using this function is similar to the [official](https://tanstack.com/query/v5/docs/guides/infinite-queries) function.

```ts
import { injectInfiniteQuery } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class PostsService {
#query = injectInfiniteQuery();

getPosts() {
return this.#query({
queryKey: ['posts'],
queryFn: ({ pageParam }) => getProjects(pageParam),
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId,
getNextPageParam: (lastPage) => lastPage.nextId,
});
}
}
```

> The function should run inside an injection context

## Mutation

Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, The library exports the `injectMutation`` function.

```ts
import { injectMutation } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
#mutation = injectMutation();
#http = inject(HttpClient);

addTodo() {
return this.#mutation({
mutationFn: ({ title }) =>
this.#http.post(`https://jsonplaceholder.typicode.com/todos`, {
title,
}),
});
}
}
```

The `variables` in the `mutationFn` callback are the variables that will be passed to the `mutate` function later.

Now create your component in which you want to use your newly created service:

```ts
@Component({
template: `

Add todo

@if (addTodo.result$ | async; as result) {
@if (result.isLoading) {

Mutation is loading


}
@if (result.isSuccess) {

Mutation was successful


}
@if (result.isError) {

Mutation encountered an Error


}
}
`,
})
export class TodosComponent {
addTodo = inject(TodosService).addTodo();

onAddTodo({ title }) {
this.addTodo.mutate({ title });
// Or
this.addTodo.mutateAsync({ title });
}
}
```

If you prefer a signal based approach, then you can use the `result` getter function on `addTodo`.

```ts
@Component({
template: `

Add todo

@if (addTodo.result(); as result) {
@if (result.isLoading) {

Mutation is loading


}
@if (result.isSuccess) {

Mutation was successful


}
@if (result.isError) {

Mutation encountered an Error


}
}
`,
})
export class TodosComponent {
addTodo = inject(TodosService).addTodo();

onAddTodo({ title }) {
this.addTodo.mutate({ title });
}
}
```

A more in depth [example](https://github.com/ngneat/query/blob/next/src/app/mutation-page/) can be found on our playground.

## Query Global Options

You can inject a default config for the underlying `@tanstack/query` instance by using the `provideQueryClientOptions({})` function.

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

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

It accept also a function factory if you need an injection context while creating the configuration.

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

const withFunctionalFactory: QueryClientConfigFn = () => {
const notificationService = inject(NotificationService);

return {
queryCache: new QueryCache({
onError: (error: Error) => notificationService.notifyError(error),
}),
defaultOptions: {
queries: {
staleTime: 3000,
},
},
};
};

bootstrapApplication(AppComponent, {
providers: [provideQueryClientOptions(withFunctionalFactory)],
});
```

## Signal Utils

### intersectResults

The `intersectResults` function is used to merge multiple **_signal_** queries into one.
It will return a new base query result that will merge the results of all the queries.

> **Note:** The data will only be mapped if the result is **successful** and otherwise just returned as is on **any other** state.

```ts
import { intersectResults } from '@ngneat/query';

@Component({
standalone: true,
template: `

Signals Intersection


@if (intersection(); as intersectionResult) {
@if (intersectionResult.isLoading) {

Loading


}
@if (intersectionResult.isSuccess) {

{{ intersectionResult.data }}


}
@if (intersectionResult.isError) {

Error


}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosPageComponent {
#todosService = inject(TodosService);

intersection = intersectResults(
[
this.#todosService.getTodo('1').result,
this.#todosService.getTodo('2').result,
],
([todoOne, todoTwo]) => todoOne.title + todoTwo.title,
);

intersectionAsObject = intersectResults(
{
todoOne: this.#todosService.getTodo('1').result,
todoTwo: this.#todosService.getTodo('2').result,
},
({ todoOne, todoTwo }) => todoOne.title + todoTwo.title,
);
}
```

## RxJS Operators

### filterSuccessResult

The `filterSuccessResult` operator is useful when you want to filter only successful results:

`todosService.getTodos().result$.pipe(filterSuccessResult())`

### filterErrorResult

The `filterErrorResult` operator is useful when you want to filter only error results:

`todosService.getTodos().result$.pipe(filterErrorResult())`

### tapSuccessResult

The `tapSuccessResult` operator is useful when you want to run a side effect only when the result is successful:

`todosService.getTodos().result$.pipe(tapSuccessResult(console.log))`

### tapErrorResult

The `tapErrorResult` operator is useful when you want to run a side effect only when the result is erroring:

`todosService.getTodos().result$.pipe(tapErrorResult(console.log))`

### mapResultData

The `mapResultData` operator maps the `data` property of the `result` object in case of a successful result.

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

### takeUntilResultFinalize

An operator that takes values emitted by the source observable until the `isFetching` property on the result is false.
It is intended to be used in scenarios where an observable stream should be listened to until the result has finished fetching (e.g success or error).

`todosService.getTodos().result$.pipe(takeUntilResultFinalize())`

### takeUntilResultSuccess

An operator that takes values emitted by the source observable until the `isSuccess` property on the result is true.
It is intended to be used in scenarios where an observable stream should be listened to until a successful result is emitted.

`todosService.getTodos().result$.pipe(takeUntilResultSuccess())`

### takeUntilResultError()

An operator that takes values emitted by the source observable until the `isError` property on the result is true.
It is intended to be used in scenarios where an observable stream should be listened to until an error result is emitted.

`todosService.getTodos().result$.pipe(takeUntilResultError())`

### startWithPendingQueryResult

Starts the observable stream with a pending query result that would also be returned upon creating a normal query:

```ts
this.todosService.getTodos().result$.pipe(
filterSuccess(),
switchMap(() => someSource),
startWithPendingQueryResult(),
);
```

### intersectResults$

The `intersectResults$` operator is used to merge multiple **_observable_** queries into one, this is usually done with a `combineLatest`.
It will return a new base query result that will merge the results of all the queries.

> **Note:** The data will only be mapped if the result is **successful** and otherwise just returned as is on **any other** state.

```ts
const query = combineLatest({
todos: todos.result$,
posts: posts.result$,
}).pipe(
intersectResults$(({ todos, posts }) => { ... })
)

const query = combineLatest([todos.result$, posts.result$]).pipe(
intersectResults$(([todos, posts]) => { ... })
)
```

## Utils

- `createSuccessObserverResult` - Create success observer result:

```
import { createSyncObserverResult } from '@ngneat/query';

result = of(createSuccessObserverResult(data))
```

- `createPendingObserverResult` - Create pending observer result

- `updateOptions` - In cases that you want to use the same observer result and update the options you can use the `updateOptions` method:

```ts
@Component({
standalone: true,
imports: [RouterModule],
template: `
User 2

@if (user().isLoading) {

}

@if (user().data; as user) {
{{ user.email }}
}
`,
})
export class UsersPageComponent {
usersService = inject(UsersService);
userResultDef = this.usersService.getUser(
+inject(ActivatedRoute).snapshot.queryParams['id'],
);

user = this.userResultDef.result;

@Input()
set id(userId: string) {
this.userResultDef.updateOptions(this.usersService.getUserOptions(+userId));
}
}
```

## Type Utils

- `ObservableQueryResult` - Alias for `Observable>`
- `SignalQueryResult` - Alias for `Signal>`

## Is Fetching

`injectIsFetching` is a function that returns the number of the queries that your application is loading or fetching in the background (useful for app-wide loading indicators).

### Observable Example

```ts
import { injectIsFetching } from '@ngneat/query';

class TodoComponent {
#isFetching = injectIsFetching();

// How many queries overall are currently fetching data?
public isFetching$ = this.#isFetching().result$;

// How many queries matching the todos prefix are currently fetching?
public isFetchingTodos$ = this.#isFetching({ queryKey: ['todos'] }).result$;
}
```

### Signal Example

```ts
import { injectIsFetching } from '@ngneat/query';

class TodoComponent {
#isFetching = injectIsFetching();

// How many queries overall are currently fetching data?
public isFetching = this.#isFetching().result;

// How many queries matching the todos prefix are currently fetching?
public isFetchingTodos = this.#isFetching({
queryKey: ['todos'],
}).result;
}
```

## Is Mutating

`injectIsMutating` is an optional hook that returns the number of mutations that your application is fetching (useful for app-wide loading indicators).

### Observable Example

```ts
import { injectIsMutating } from '@ngneat/query';

class TodoComponent {
#isMutating = injectIsMutating();

// How many queries overall are currently fetching data?
public isFetching$ = this.#isMutating().result$;

// How many queries matching the todos prefix are currently fetching?
public isFetchingTodos$ = this.#isMutating({ queryKey: ['todos'] }).result$;
}
```

### Signal Example

```ts
import { injectIsMutating } from '@ngneat/query';

class TodoComponent {
#isMutating = injectIsMutating();

// How many queries overall are currently fetching data?
public isFetching = this.#isMutating().result;

// How many queries matching the todos prefix are currently fetching?
public isFetchingTodos = this.#isMutating({
queryKey: ['todos'],
}).result;
}
```

## Devtools

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

```ts
import { provideQueryDevTools } from '@ngneat/query-devtools';
import { environment } from 'src/environments/environment';

bootstrapApplication(AppComponent, {
providers: [environment.production ? [] : provideQueryDevTools(options)],
});
```

See all the avilable options [here](https://tanstack.com/query/v5/docs/react/devtools#options).

## SSR

On the Server:

```ts
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(
'