Skip to content

Commit 7d3abb8

Browse files
authored
fix(angular-query): do not run callbacks in injection context (#8817)
1 parent 1dba812 commit 7d3abb8

File tree

7 files changed

+31
-175
lines changed

7 files changed

+31
-175
lines changed

packages/angular-query-devtools-experimental/src/inject-devtools-panel.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ export function injectDevtoolsPanel(
3838
const currentInjector = injector ?? inject(Injector)
3939

4040
return runInInjectionContext(currentInjector, () => {
41+
const destroyRef = inject(DestroyRef)
42+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID))
43+
const injectedClient = inject(QueryClient, { optional: true })
44+
4145
const options = computed(optionsFn)
4246
let devtools: TanstackQueryDevtoolsPanel | null = null
4347

44-
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID))
45-
4648
const destroy = () => {
4749
devtools?.unmount()
4850
devtools = null
@@ -53,10 +55,7 @@ export function injectDevtoolsPanel(
5355
destroy,
5456
}
5557

56-
const destroyRef = inject(DestroyRef)
57-
5858
effect(() => {
59-
const injectedClient = currentInjector.get(QueryClient, null)
6059
const {
6160
client = injectedClient,
6261
errorTypes = [],

packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ describe('injectMutationState', () => {
143143
@Component({
144144
selector: 'app-fake',
145145
template: `
146-
@for (mutation of mutationState(); track mutation) {
146+
@for (mutation of mutationState(); track $index) {
147147
<span>{{ mutation.status }}</span>
148148
}
149149
`,

packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts

-43
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {
22
Component,
3-
Injectable,
43
Injector,
5-
inject,
64
input,
75
provideExperimentalZonelessChangeDetection,
86
signal,
@@ -451,47 +449,6 @@ describe('injectMutation', () => {
451449
await expect(() => mutateAsync()).rejects.toThrowError(err)
452450
})
453451

454-
test('should execute callback in injection context', async () => {
455-
const errorSpy = vi.fn()
456-
@Injectable()
457-
class FakeService {
458-
updateData(name: string) {
459-
return Promise.resolve(name)
460-
}
461-
}
462-
463-
@Component({
464-
selector: 'app-fake',
465-
template: ``,
466-
standalone: true,
467-
providers: [FakeService],
468-
})
469-
class FakeComponent {
470-
mutation = injectMutation(() => {
471-
try {
472-
const service = inject(FakeService)
473-
return {
474-
mutationFn: (name: string) => service.updateData(name),
475-
}
476-
} catch (e) {
477-
errorSpy(e)
478-
throw e
479-
}
480-
})
481-
}
482-
483-
const fixture = TestBed.createComponent(FakeComponent)
484-
fixture.detectChanges()
485-
486-
// check if injection contexts persist in a different task
487-
await new Promise<void>((resolve) => queueMicrotask(() => resolve()))
488-
489-
expect(
490-
await fixture.componentInstance.mutation.mutateAsync('test'),
491-
).toEqual('test')
492-
expect(errorSpy).not.toHaveBeenCalled()
493-
})
494-
495452
describe('injection context', () => {
496453
test('throws NG0203 with descriptive error outside injection context', () => {
497454
expect(() => {

packages/angular-query-experimental/src/__tests__/inject-query.test.ts

-87
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import {
22
Component,
3-
Injectable,
43
Injector,
54
computed,
65
effect,
7-
inject,
86
input,
97
provideExperimentalZonelessChangeDetection,
108
signal,
@@ -537,91 +535,6 @@ describe('injectQuery', () => {
537535
)
538536
})
539537

540-
test('should run optionsFn in injection context', async () => {
541-
@Injectable()
542-
class FakeService {
543-
getData(name: string) {
544-
return Promise.resolve(name)
545-
}
546-
}
547-
548-
@Component({
549-
selector: 'app-fake',
550-
template: `{{ query.data() }}`,
551-
standalone: true,
552-
providers: [FakeService],
553-
})
554-
class FakeComponent {
555-
name = signal<string>('test name')
556-
557-
query = injectQuery(() => {
558-
const service = inject(FakeService)
559-
560-
return {
561-
queryKey: ['fake', this.name()],
562-
queryFn: () => {
563-
return service.getData(this.name())
564-
},
565-
}
566-
})
567-
}
568-
569-
const fixture = TestBed.createComponent(FakeComponent)
570-
fixture.detectChanges()
571-
await resolveQueries()
572-
573-
expect(fixture.componentInstance.query.data()).toEqual('test name')
574-
575-
fixture.componentInstance.name.set('test name 2')
576-
fixture.detectChanges()
577-
await resolveQueries()
578-
579-
expect(fixture.componentInstance.query.data()).toEqual('test name 2')
580-
})
581-
582-
test('should run optionsFn in injection context and allow passing injector to queryFn', async () => {
583-
@Injectable()
584-
class FakeService {
585-
getData(name: string) {
586-
return Promise.resolve(name)
587-
}
588-
}
589-
590-
@Component({
591-
selector: 'app-fake',
592-
template: `{{ query.data() }}`,
593-
standalone: true,
594-
providers: [FakeService],
595-
})
596-
class FakeComponent {
597-
name = signal<string>('test name')
598-
599-
query = injectQuery(() => {
600-
const injector = inject(Injector)
601-
602-
return {
603-
queryKey: ['fake', this.name()],
604-
queryFn: () => {
605-
const service = injector.get(FakeService)
606-
return service.getData(this.name())
607-
},
608-
}
609-
})
610-
}
611-
612-
const fixture = TestBed.createComponent(FakeComponent)
613-
fixture.detectChanges()
614-
await resolveQueries()
615-
616-
expect(fixture.componentInstance.query.data()).toEqual('test name')
617-
618-
fixture.componentInstance.name.set('test name 2')
619-
fixture.detectChanges()
620-
await resolveQueries()
621-
622-
expect(fixture.componentInstance.query.data()).toEqual('test name 2')
623-
})
624-
625538
describe('injection context', () => {
626539
test('throws NG0203 with descriptive error outside injection context', () => {
627540
expect(() => {

packages/angular-query-experimental/src/create-base-query.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import {
22
DestroyRef,
3-
Injector,
43
NgZone,
54
VERSION,
65
computed,
76
effect,
87
inject,
9-
runInInjectionContext,
108
signal,
119
untracked,
1210
} from '@angular/core'
@@ -39,10 +37,9 @@ export function createBaseQuery<
3937
>,
4038
Observer: typeof QueryObserver,
4139
) {
42-
const injector = inject(Injector)
43-
const ngZone = injector.get(NgZone)
44-
const destroyRef = injector.get(DestroyRef)
45-
const queryClient = injector.get(QueryClient)
40+
const ngZone = inject(NgZone)
41+
const destroyRef = inject(DestroyRef)
42+
const queryClient = inject(QueryClient)
4643

4744
/**
4845
* Signal that has the default options from query client applied
@@ -51,8 +48,7 @@ export function createBaseQuery<
5148
* are preserved and can keep being applied after signal changes
5249
*/
5350
const defaultedOptionsSignal = computed(() => {
54-
const options = runInInjectionContext(injector, () => optionsFn())
55-
const defaultedOptions = queryClient.defaultQueryOptions(options)
51+
const defaultedOptions = queryClient.defaultQueryOptions(optionsFn())
5652
defaultedOptions._optimisticResults = 'optimistic'
5753
return defaultedOptions
5854
})
@@ -100,7 +96,6 @@ export function createBaseQuery<
10096
// Set allowSignalWrites to support Angular < v19
10197
// Set to undefined to avoid warning on newer versions
10298
allowSignalWrites: VERSION.major < '19' || undefined,
103-
injector,
10499
},
105100
)
106101

packages/angular-query-experimental/src/inject-mutation.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {
22
DestroyRef,
3-
Injector,
43
NgZone,
54
computed,
65
effect,
76
inject,
8-
runInInjectionContext,
97
signal,
108
untracked,
119
} from '@angular/core'
@@ -17,6 +15,7 @@ import {
1715
import { assertInjector } from './util/assert-injector/assert-injector'
1816
import { signalProxy } from './signal-proxy'
1917
import { noop, shouldThrowError } from './util'
18+
import type { Injector } from '@angular/core'
2019
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
2120
import type { CreateMutateFunction, CreateMutationResult } from './types'
2221
import type { CreateMutationOptions } from './mutation-options'
@@ -40,7 +39,6 @@ export function injectMutation<
4039
injector?: Injector,
4140
): CreateMutationResult<TData, TError, TVariables, TContext> {
4241
return assertInjector(injectMutation, injector, () => {
43-
const currentInjector = inject(Injector)
4442
const destroyRef = inject(DestroyRef)
4543
const ngZone = inject(NgZone)
4644
const queryClient = inject(QueryClient)
@@ -50,9 +48,7 @@ export function injectMutation<
5048
* making it reactive. Wrapping options in a function ensures embedded expressions
5149
* are preserved and can keep being applied after signal changes
5250
*/
53-
const optionsSignal = computed(() =>
54-
runInInjectionContext(currentInjector, () => optionsFn()),
55-
)
51+
const optionsSignal = computed(optionsFn)
5652

5753
const observerSignal = (() => {
5854
let instance: MutationObserver<

packages/angular-query-experimental/src/providers.ts

+20-24
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import {
22
DestroyRef,
33
ENVIRONMENT_INITIALIZER,
4-
Injector,
54
PLATFORM_ID,
65
computed,
76
effect,
87
inject,
98
makeEnvironmentProviders,
10-
runInInjectionContext,
119
} from '@angular/core'
1210
import { QueryClient, onlineManager } from '@tanstack/query-core'
1311
import { isPlatformBrowser } from '@angular/common'
@@ -99,7 +97,7 @@ export function provideTanStackQuery(
9997
return makeEnvironmentProviders([
10098
provideQueryClient(queryClient),
10199
{
102-
// Do not use provideEnvironmentInitializer to support Angular < v19
100+
// Do not use provideEnvironmentInitializer while Angular < v19 is supported
103101
provide: ENVIRONMENT_INITIALIZER,
104102
multi: true,
105103
useValue: () => {
@@ -250,14 +248,17 @@ export function withDevtools(
250248
} else {
251249
providers = [
252250
{
251+
// Do not use provideEnvironmentInitializer while Angular < v19 is supported
253252
provide: ENVIRONMENT_INITIALIZER,
254253
multi: true,
255254
useFactory: () => {
256255
if (!isPlatformBrowser(inject(PLATFORM_ID))) return noop
257-
const injector = inject(Injector)
258-
const options = computed(() =>
259-
runInInjectionContext(injector, () => optionsFn?.() ?? {}),
260-
)
256+
const injectedClient = inject(QueryClient, {
257+
optional: true,
258+
})
259+
const destroyRef = inject(DestroyRef)
260+
261+
const options = computed(() => optionsFn?.() ?? {})
261262

262263
let devtools: TanstackQueryDevtools | null = null
263264
let el: HTMLElement | null = null
@@ -269,10 +270,7 @@ export function withDevtools(
269270
: isDevMode()
270271
})
271272

272-
const destroyRef = inject(DestroyRef)
273-
274273
const getResolvedQueryClient = () => {
275-
const injectedClient = injector.get(QueryClient, null)
276274
const client = options().client ?? injectedClient
277275
if (!client) {
278276
throw new Error('No QueryClient found')
@@ -314,22 +312,20 @@ export function withDevtools(
314312
el = document.body.appendChild(document.createElement('div'))
315313
el.classList.add('tsqd-parent-container')
316314

317-
import('@tanstack/query-devtools').then((queryDevtools) =>
318-
runInInjectionContext(injector, () => {
319-
devtools = new queryDevtools.TanstackQueryDevtools({
320-
...options(),
321-
client: getResolvedQueryClient(),
322-
queryFlavor: 'Angular Query',
323-
version: '5',
324-
onlineManager,
325-
})
315+
import('@tanstack/query-devtools').then((queryDevtools) => {
316+
devtools = new queryDevtools.TanstackQueryDevtools({
317+
...options(),
318+
client: getResolvedQueryClient(),
319+
queryFlavor: 'Angular Query',
320+
version: '5',
321+
onlineManager,
322+
})
326323

327-
el && devtools.mount(el)
324+
el && devtools.mount(el)
328325

329-
// Unmount the devtools on application destroy
330-
destroyRef.onDestroy(destroyDevtools)
331-
}),
332-
)
326+
// Unmount the devtools on application destroy
327+
destroyRef.onDestroy(destroyDevtools)
328+
})
333329
})
334330
},
335331
},

0 commit comments

Comments
 (0)