Skip to content

Commit 093e053

Browse files
committed
feat(query): 🔥 add experimental signal support
1 parent 0c74412 commit 093e053

File tree

13 files changed

+83
-65
lines changed

13 files changed

+83
-65
lines changed

.eslintrc.base.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
{
2525
"files": ["*.ts", "*.tsx"],
2626
"extends": ["plugin:@nx/typescript"],
27-
"rules": {}
27+
"rules": {
28+
"@typescript-eslint/no-explicit-any": "off"
29+
}
2830
},
2931
{
3032
"files": ["*.js", "*.jsx"],

.github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
- name: Install dependencies
3232
run: npm i
3333

34+
- name: Run Lint
35+
run: npm run lint:all
36+
3437
- name: Run Build
3538
run: npm run build:all
3639

devtools/.eslintrc.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
{
3636
"files": ["*.json"],
3737
"parser": "jsonc-eslint-parser",
38-
"rules": {
39-
"@nx/dependency-checks": "error"
40-
}
38+
"rules": {}
4139
}
4240
]
4341
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"build:devtools": "nx build devtools",
1010
"build:all": "nx run-many --target=build",
1111
"test:all": "nx run-many --target=test",
12+
"lint:all": "nx run-many --target=lint",
1213
"start": "nx serve",
1314
"format": "nx format:write --all",
1415
"update": "nx migrate latest",

query/.eslintrc.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
{
3636
"files": ["*.json"],
3737
"parser": "jsonc-eslint-parser",
38-
"rules": {
39-
"@nx/dependency-checks": "error"
40-
}
38+
"rules": {}
4139
}
4240
]
4341
}

query/src/lib/base-query.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { Observable, shareReplay } from 'rxjs';
1111
import { toSignal } from '@angular/core/rxjs-interop';
1212
import { Result } from './types';
13+
import { assertInInjectionContext } from '@angular/core';
1314

1415
export type CreateBaseQueryOptions<
1516
TQueryFnData = unknown,
@@ -60,11 +61,9 @@ export function createBaseQuery<
6061
>(client, defaultedOptions);
6162

6263
const result$ = new Observable((observer) => {
63-
const mergedOptions = client.defaultQueryOptions({
64-
...options,
65-
// The query key can be changed, so we need to rebuild it each time
66-
...queryObserver.options,
67-
});
64+
const mergedOptions = client.defaultQueryOptions(
65+
client.defaultQueryOptions(options)
66+
);
6867

6968
observer.next(queryObserver.getOptimisticResult(mergedOptions));
7069

@@ -89,6 +88,18 @@ export function createBaseQuery<
8988
return {
9089
result$,
9190
setOptions: queryObserver.setOptions.bind(queryObserver),
92-
toSignal: () => toSignal(result$),
91+
__cached__: undefined,
92+
// @experimental signal support
93+
get result() {
94+
assertInInjectionContext(function queryResultSignal() {
95+
// noop
96+
});
97+
98+
if (!this.__cached__) {
99+
this.__cached__ = toSignal(this.result$);
100+
}
101+
102+
return this.__cached__;
103+
},
93104
};
94105
}

query/src/lib/mutation.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
MutationObserverResult,
88
notifyManager,
99
} from '@tanstack/query-core';
10-
import { firstValueFrom, isObservable, Observable, shareReplay } from 'rxjs';
10+
import { isObservable, Observable, shareReplay } from 'rxjs';
1111
import { toSignal } from '@angular/core/rxjs-interop';
12+
import { toPromise } from './utils';
1213

1314
type CreateMutationOptions<
1415
TData = unknown,
@@ -40,9 +41,7 @@ type MutationResult<
4041
result$: Observable<
4142
MutationObserverResult<TData, TError, TVariables, TContext>
4243
>;
43-
toSignal: () => Signal<
44-
MutationObserverResult<TData, TError, TVariables, TContext>
45-
>;
44+
result: Signal<MutationObserverResult<TData, TError, TVariables, TContext>>;
4645
};
4746

4847
@Injectable({ providedIn: 'root' })
@@ -65,10 +64,12 @@ class Mutation {
6564
>(this.instance, {
6665
...options,
6766
mutationFn: (variables: TVariables): Promise<TData> => {
68-
const result: Promise<TData> | Observable<TData> =
67+
const source: Promise<TData> | Observable<TData> =
6968
options.mutationFn(variables);
70-
if (isObservable(result)) return firstValueFrom(result);
71-
return result;
69+
70+
if (isObservable(source)) return toPromise({ source });
71+
72+
return source;
7273
},
7374
});
7475

@@ -94,17 +95,26 @@ class Mutation {
9495
);
9596

9697
const mutate = (variables: TVariables) => {
97-
mutationObserver.mutate(variables).catch(() => {});
98+
mutationObserver.mutate(variables).catch(() => {
99+
// noop
100+
});
98101
};
99102

100103
return {
101104
mutate,
102-
mutateAsync: mutationObserver.mutate.bind(mutationObserver),
103105
reset: mutationObserver.reset.bind(mutationObserver),
104106
setOptions: mutationObserver.setOptions.bind(mutationObserver),
105107
result$,
106-
toSignal: () => toSignal(result$, { requireSync: true }),
107-
};
108+
__cached__: undefined,
109+
// @experimental signal support
110+
get result() {
111+
if (!this.__cached__) {
112+
this.__cached__ = toSignal(this.result$);
113+
}
114+
115+
return this.__cached__;
116+
},
117+
} as any;
108118
}
109119
}
110120

query/src/lib/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export type ObservableQueryResult<T> = Observable<QueryObserverResult<T>>;
66

77
export type Result<T> = {
88
result$: Observable<T>;
9-
toSignal(): Signal<T>;
9+
result: Signal<T>;
1010
setOptions: QueryObserver['setOptions'];
1111
};

query/src/lib/utils.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ export function toPromise<T>({
55
signal,
66
}: {
77
source: Observable<T>;
8-
signal: AbortSignal;
8+
signal?: AbortSignal;
99
}): Promise<T> {
1010
const cancel = new Subject<void>();
1111

12-
signal.addEventListener('abort', () => {
13-
cancel.next();
14-
cancel.complete();
15-
});
12+
if (signal) {
13+
signal.addEventListener('abort', () => {
14+
cancel.next();
15+
cancel.complete();
16+
});
17+
}
1618

17-
return firstValueFrom(source.pipe(takeUntil(cancel)));
19+
return firstValueFrom(source.pipe(signal ? takeUntil(cancel) : (s) => s));
1820
}
+10-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
<ng-container *ngIf="todos$ | async as todos">
1+
<ng-container *ngIf="todosResult.result$ | async as todos">
22
<p *ngIf="todos.isPending">loading</p>
33
<p *ngIf="todos.isSuccess">
44
{{ todos.data[0].title }}
55
</p>
6+
<p *ngIf="todos.isError">Error</p>
67
</ng-container>
78

8-
<h1>Singals</h1>
9-
<p *ngIf="todos().isLoading">loading</p>
10-
<p *ngIf="todos().isSuccess">
11-
{{ todos().data?.[0]?.title }}
12-
</p>
9+
<h1>Signals</h1>
10+
<ng-container *ngIf="todos() as result">
11+
<p *ngIf="result.isLoading">loading</p>
12+
<p *ngIf="result.isSuccess">
13+
{{ result.data[0].title }}
14+
</p>
15+
<p *ngIf="result.isError">Error</p>
16+
</ng-container>

src/app/basic-page/todos-page.component.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ import { TodosService } from '../services/todos.service';
1111
changeDetection: ChangeDetectionStrategy.OnPush,
1212
})
1313
export class TodosPageComponent {
14-
todos$ = inject(TodosService).getTodos().result$;
15-
todos = inject(TodosService).getTodos().toSignal();
14+
todosResult = inject(TodosService).getTodos();
15+
todos = this.todosResult.result;
1616
}

src/app/mutation-page/mutation-page.component.ts

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
22
import { CommonModule } from '@angular/common';
3-
import { injectIsMutating, injectMutation } from '@ngneat/query';
4-
import { HttpClient } from '@angular/common/http';
3+
import { injectIsMutating } from '@ngneat/query';
4+
55
import { FormsModule } from '@angular/forms';
66
import { TodosService } from '../services/todos.service';
7-
8-
interface Todo {
9-
id: number;
10-
title: string;
11-
completed: boolean;
12-
}
13-
147
@Component({
158
selector: 'query-mutation-page',
169
standalone: true,
@@ -25,9 +18,8 @@ export class MutationPageComponent {
2518

2619
public addTodoMutationsActive = this.useIsMutating().toSignal();
2720

28-
2921
public addTodo = this.todoService.addTodo();
30-
public addTodoSignalResult = this.addTodo.toSignal();
22+
public addTodoSignalResult = this.addTodo.result;
3123
public newTodo = '';
3224

3325
public onAddTodo(title: string) {

src/app/services/todos.service.ts

+12-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HttpClient } from '@angular/common/http';
22
import { Injectable, inject } from '@angular/core';
3-
import { injectMutation, injectQuery, queryOptions, toPromise } from '@ngneat/query';
3+
import { injectMutation, injectQuery, toPromise } from '@ngneat/query';
44

55
interface Todo {
66
id: number;
@@ -13,17 +13,6 @@ export class TodosService {
1313
private useMutation = injectMutation();
1414
private http = inject(HttpClient);
1515

16-
todosQuery = queryOptions({
17-
queryKey: ['todos'] as const,
18-
queryFn: ({ signal }) => {
19-
const source = this.http.get<Todo[]>(
20-
'https://jsonplaceholder.typicode.com/todos'
21-
);
22-
23-
return toPromise({ source, signal });
24-
},
25-
});
26-
2716
getTodos() {
2817
return this.query({
2918
queryKey: ['todos'] as const,
@@ -39,12 +28,20 @@ export class TodosService {
3928

4029
addTodo() {
4130
return this.useMutation({
42-
mutationFn: ({title, showError}: {title: string, showError: boolean}) => {
43-
const url = showError ? 'https://jsonplaceholder.typicode.com/error/404' : 'https://jsonplaceholder.typicode.com/todos';
31+
mutationFn: ({
32+
title,
33+
showError,
34+
}: {
35+
title: string;
36+
showError: boolean;
37+
}) => {
38+
const url = showError
39+
? 'https://jsonplaceholder.typicode.com/error/404'
40+
: 'https://jsonplaceholder.typicode.com/todos';
4441
return this.http.post<Todo>(url, {
4542
title,
4643
});
47-
}
44+
},
4845
});
4946
}
5047
}

0 commit comments

Comments
 (0)