From a5399cd24dbba2266a506f81751918df3ee1f007 Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:34:22 +0200 Subject: [PATCH 1/3] refactor(angular-query): improve devtools tree shaking --- docs/framework/angular/devtools.md | 4 +- .../src/with-persist-query-client.ts | 3 +- .../src/__tests__/providers.test.ts | 155 +++++++++++++----- .../src/devtools-setup.ts | 85 ++++++++++ .../src/providers.ts | 128 ++++----------- .../src/util/is-dev-mode/is-dev-mode.ts | 3 - 6 files changed, 232 insertions(+), 146 deletions(-) create mode 100644 packages/angular-query-experimental/src/devtools-setup.ts delete mode 100644 packages/angular-query-experimental/src/util/is-dev-mode/is-dev-mode.ts diff --git a/docs/framework/angular/devtools.md b/docs/framework/angular/devtools.md index 1787544ca1..b5149f6f3a 100644 --- a/docs/framework/angular/devtools.md +++ b/docs/framework/angular/devtools.md @@ -7,7 +7,7 @@ title: Devtools The devtools help you debug and inspect your queries and mutations. You can enable the devtools by adding `withDevtools` to `provideTanStackQuery`. -By default, the devtools are enabled when Angular [`isDevMode`](https://angular.dev/api/core/isDevMode) returns true. So you don't need to worry about excluding them during a production build. The core tools are lazily loaded and excluded from bundled code. In most cases, all you'll need to do is add `withDevtools()` to `provideTanStackQuery` without any additional configuration. +By default, the devtools are enabled when Angular is in development mode. So you don't need to worry about excluding them during a production build. The tools are lazily loaded and excluded from bundled code. In most cases, all you'll need to do is add `withDevtools()` to `provideTanStackQuery` without any additional configuration. ```ts import { @@ -61,7 +61,7 @@ Using this technique allows you to support on-demand loading of the devtools eve ```ts @Injectable({ providedIn: 'root' }) -class DevtoolsOptionsManager { +export class DevtoolsOptionsManager { loadDevtools = toSignal( fromEvent(document, 'keydown').pipe( map( diff --git a/packages/angular-persist-query-client/src/with-persist-query-client.ts b/packages/angular-persist-query-client/src/with-persist-query-client.ts index 2896cbc7b2..16ef8cf8ef 100644 --- a/packages/angular-persist-query-client/src/with-persist-query-client.ts +++ b/packages/angular-persist-query-client/src/with-persist-query-client.ts @@ -1,5 +1,6 @@ import { QueryClient, + QueryFeatureKind, provideIsRestoring, queryFeature, } from '@tanstack/angular-query-experimental' @@ -84,5 +85,5 @@ export function withPersistQueryClient( }, }, ] - return queryFeature('PersistQueryClient', providers) + return queryFeature(QueryFeatureKind.PersistQueryClient, providers) } diff --git a/packages/angular-query-experimental/src/__tests__/providers.test.ts b/packages/angular-query-experimental/src/__tests__/providers.test.ts index 266aa40a35..41755ab99c 100644 --- a/packages/angular-query-experimental/src/__tests__/providers.test.ts +++ b/packages/angular-query-experimental/src/__tests__/providers.test.ts @@ -1,25 +1,24 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest' +import { beforeEach, describe, expect, it, test, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' import { TestBed } from '@angular/core/testing' import { ENVIRONMENT_INITIALIZER, + PLATFORM_ID, provideExperimentalZonelessChangeDetection, signal, } from '@angular/core' -import { isDevMode } from '../util/is-dev-mode/is-dev-mode' -import { provideTanStackQuery, withDevtools } from '../providers' +import { + QueryFeatureKind, + provideTanStackQuery, + withDevtools, +} from '../providers' import type { DevtoolsOptions } from '../providers' -import type { Mock } from 'vitest' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, } from '@tanstack/query-devtools' -vi.mock('../util/is-dev-mode/is-dev-mode', () => ({ - isDevMode: vi.fn(), -})) - const mockDevtoolsInstance = { mount: vi.fn(), unmount: vi.fn(), @@ -37,104 +36,170 @@ vi.mock('@tanstack/query-devtools', () => ({ })) describe('withDevtools feature', () => { - let isDevModeMock: Mock - beforeEach(() => { vi.useFakeTimers() - isDevModeMock = isDevMode as Mock }) afterEach(() => { vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + describe('tree shaking', () => { + it('should return empty providers when ngDevMode and withDevtoolsFn are undefined', () => { + vi.stubGlobal('ngDevMode', undefined) + const feature = withDevtools() + expect(feature.ɵkind).toEqual(QueryFeatureKind.DeveloperTools) + expect(feature.ɵproviders.length).toEqual(0) + }) + + it('should return providers when ngDevMode is undefined and withDevtoolsFn is defined', () => { + vi.stubGlobal('ngDevMode', undefined) + const feature = withDevtools(() => ({})) + expect(feature.ɵkind).toEqual(QueryFeatureKind.DeveloperTools) + expect(feature.ɵproviders.length).toBeGreaterThan(0) + }) + + it('should return providers when ngDevMode is defined and withDevtoolsFn is undefined', () => { + vi.stubGlobal('ngDevMode', {}) + const feature = withDevtools() + expect(feature.ɵkind).toEqual(QueryFeatureKind.DeveloperTools) + expect(feature.ɵproviders.length).toBeGreaterThan(0) + }) }) test.each([ { description: 'should provide developer tools in development mode by default', - isDevModeValue: true, + isDevMode: true, expectedCalled: true, }, { description: 'should not provide developer tools in production mode by default', - isDevModeValue: false, + isDevMode: false, expectedCalled: false, }, { description: `should provide developer tools in development mode when 'loadDeveloperTools' is set to 'auto'`, - isDevModeValue: true, + isDevMode: true, loadDevtools: 'auto', expectedCalled: true, }, { description: `should not provide developer tools in production mode when 'loadDeveloperTools' is set to 'auto'`, - isDevModeValue: false, + isDevMode: false, loadDevtools: 'auto', expectedCalled: false, }, { description: "should provide developer tools in development mode when 'loadDevtools' is set to true", - isDevModeValue: true, + isDevMode: true, loadDevtools: true, expectedCalled: true, }, { description: "should provide developer tools in production mode when 'loadDevtools' is set to true", - isDevModeValue: false, + isDevMode: false, loadDevtools: true, expectedCalled: true, }, { description: "should not provide developer tools in development mode when 'loadDevtools' is set to false", - isDevModeValue: true, + isDevMode: true, loadDevtools: false, expectedCalled: false, }, { description: "should not provide developer tools in production mode when 'loadDevtools' is set to false", - isDevModeValue: false, + isDevMode: false, loadDevtools: false, expectedCalled: false, }, - ])( - '$description', - async ({ isDevModeValue, loadDevtools, expectedCalled }) => { - isDevModeMock.mockReturnValue(isDevModeValue) + ])('$description', async ({ isDevMode, loadDevtools, expectedCalled }) => { + vi.stubGlobal('ngDevMode', isDevMode ? {} : undefined) + + const providers = [ + provideExperimentalZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + loadDevtools !== undefined + ? withDevtools( + () => + ({ + loadDevtools, + }) as DevtoolsOptions, + ) + : withDevtools(), + ), + ] + + TestBed.configureTestingModule({ + providers, + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await vi.runAllTimersAsync() + TestBed.flushEffects() + await vi.dynamicImportSettled() + TestBed.flushEffects() + await vi.dynamicImportSettled() + + if (expectedCalled) { + expect(mockTanstackQueryDevtools).toHaveBeenCalled() + } else { + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + } + }) - const providers = [ + it('should not load devtools if injector is destroyed', async () => { + TestBed.configureTestingModule({ + providers: [ provideExperimentalZonelessChangeDetection(), provideTanStackQuery( new QueryClient(), - loadDevtools !== undefined - ? withDevtools( - () => - ({ - loadDevtools, - }) as DevtoolsOptions, - ) - : withDevtools(), + withDevtools(() => ({ + loadDevtools: true, + })), ), - ] + ], + }) - TestBed.configureTestingModule({ - providers, - }) + TestBed.inject(ENVIRONMENT_INITIALIZER) + // Destroys injector + TestBed.resetTestingModule() + await vi.runAllTimersAsync() - TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.runAllTimersAsync() + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + }) - if (expectedCalled) { - expect(mockTanstackQueryDevtools).toHaveBeenCalled() - } else { - expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() - } - }, - ) + it('should not load devtools if platform is not browser', async () => { + TestBed.configureTestingModule({ + providers: [ + { + provide: PLATFORM_ID, + useValue: 'server', + }, + provideExperimentalZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await vi.runAllTimersAsync() + + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + }) it('should update error types', async () => { const errorTypes = signal([] as Array) diff --git a/packages/angular-query-experimental/src/devtools-setup.ts b/packages/angular-query-experimental/src/devtools-setup.ts new file mode 100644 index 0000000000..0856bdbbe2 --- /dev/null +++ b/packages/angular-query-experimental/src/devtools-setup.ts @@ -0,0 +1,85 @@ +import { DestroyRef, computed, effect } from '@angular/core' +import { QueryClient, onlineManager } from '@tanstack/query-core' +import type { Injector } from '@angular/core' +import type { TanstackQueryDevtools } from '@tanstack/query-devtools' +import type { DevtoolsOptions } from './providers' + +declare const ngDevMode: unknown + +// This function is lazy loaded to speed up up the initial load time of the application +// and to minimize bundle size +export function setupDevtools( + injector: Injector, + withDevtoolsFn?: () => DevtoolsOptions, +) { + const isDevMode = typeof ngDevMode !== 'undefined' && ngDevMode + const injectedClient = injector.get(QueryClient, { + optional: true, + }) + const destroyRef = injector.get(DestroyRef) + + const options = computed(() => withDevtoolsFn?.() ?? {}) + + let devtools: TanstackQueryDevtools | null = null + let el: HTMLElement | null = null + + const shouldLoadToolsSignal = computed(() => { + const { loadDevtools } = options() + return typeof loadDevtools === 'boolean' ? loadDevtools : isDevMode + }) + + const getResolvedQueryClient = () => { + const client = options().client ?? injectedClient + if (!client) { + throw new Error('No QueryClient found') + } + return client + } + + const destroyDevtools = () => { + devtools?.unmount() + el?.remove() + devtools = null + } + + effect( + () => { + const shouldLoadTools = shouldLoadToolsSignal() + const { client, position, errorTypes, buttonPosition, initialIsOpen } = + options() + + if (devtools && !shouldLoadTools) { + destroyDevtools() + return + } else if (devtools && shouldLoadTools) { + client && devtools.setClient(client) + position && devtools.setPosition(position) + errorTypes && devtools.setErrorTypes(errorTypes) + buttonPosition && devtools.setButtonPosition(buttonPosition) + initialIsOpen && devtools.setInitialIsOpen(initialIsOpen) + return + } else if (!shouldLoadTools) { + return + } + + el = document.body.appendChild(document.createElement('div')) + el.classList.add('tsqd-parent-container') + + import('@tanstack/query-devtools').then((queryDevtools) => { + devtools = new queryDevtools.TanstackQueryDevtools({ + ...options(), + client: getResolvedQueryClient(), + queryFlavor: 'Angular Query', + version: '5', + onlineManager, + }) + + el && devtools.mount(el) + + // Unmount the devtools on application destroy + destroyRef.onDestroy(destroyDevtools) + }) + }, + { injector }, + ) +} diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index 98ea2e04ed..1726380ab8 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -1,24 +1,23 @@ import { DestroyRef, ENVIRONMENT_INITIALIZER, + Injector, PLATFORM_ID, - computed, - effect, inject, makeEnvironmentProviders, } from '@angular/core' -import { QueryClient, onlineManager } from '@tanstack/query-core' +import { QueryClient } from '@tanstack/query-core' import { isPlatformBrowser } from '@angular/common' -import { isDevMode } from './util/is-dev-mode/is-dev-mode' import { noop } from './util' import type { EnvironmentProviders, Provider } from '@angular/core' import type { DevtoolsButtonPosition, DevtoolsErrorType, DevtoolsPosition, - TanstackQueryDevtools, } from '@tanstack/query-devtools' +declare const ngDevMode: unknown + /** * Usually {@link provideTanStackQuery} is used once to set up TanStack Query and the * {@link https://tanstack.com/query/latest/docs/reference/QueryClient|QueryClient} @@ -153,14 +152,16 @@ export function queryFeature( * @public * @see {@link withDevtools} */ -export type DeveloperToolsFeature = QueryFeature<'DeveloperTools'> +export type DeveloperToolsFeature = + QueryFeature /** * A type alias that represents a feature which enables persistence. * The type is used to describe the return value of the `withPersistQueryClient` function. * @public */ -export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'> +export type PersistQueryClientFeature = + QueryFeature /** * Options for configuring the TanStack Query devtools. @@ -249,96 +250,32 @@ export interface DevtoolsOptions { export function withDevtools( withDevtoolsFn?: () => DevtoolsOptions, ): DeveloperToolsFeature { - let providers: Array = [] - if (!isDevMode() && !withDevtoolsFn) { - providers = [] + if (withDevtoolsFn === undefined && typeof ngDevMode === 'undefined') { + return queryFeature(QueryFeatureKind.DeveloperTools, []) } else { - providers = [ - { - // Do not use provideEnvironmentInitializer while Angular < v19 is supported - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useFactory: () => { - if (!isPlatformBrowser(inject(PLATFORM_ID))) return noop - const injectedClient = inject(QueryClient, { - optional: true, - }) - const destroyRef = inject(DestroyRef) - - const options = computed(() => withDevtoolsFn?.() ?? {}) + return createDevtoolsFeature(withDevtoolsFn) + } +} - let devtools: TanstackQueryDevtools | null = null - let el: HTMLElement | null = null +function createDevtoolsFeature(withDevtoolsFn?: () => DevtoolsOptions) { + const providers = [ + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory: () => { + if (!isPlatformBrowser(inject(PLATFORM_ID))) return noop + let destroyed = false + const injector = inject(Injector) + inject(DestroyRef).onDestroy(() => (destroyed = true)) - const shouldLoadToolsSignal = computed(() => { - const { loadDevtools } = options() - return typeof loadDevtools === 'boolean' - ? loadDevtools - : isDevMode() + return () => + import('./devtools-setup').then((module) => { + !destroyed && module.setupDevtools(injector, withDevtoolsFn) }) - - const getResolvedQueryClient = () => { - const client = options().client ?? injectedClient - if (!client) { - throw new Error('No QueryClient found') - } - return client - } - - const destroyDevtools = () => { - devtools?.unmount() - el?.remove() - devtools = null - } - - return () => - effect(() => { - const shouldLoadTools = shouldLoadToolsSignal() - const { - client, - position, - errorTypes, - buttonPosition, - initialIsOpen, - } = options() - - if (devtools && !shouldLoadTools) { - destroyDevtools() - return - } else if (devtools && shouldLoadTools) { - client && devtools.setClient(client) - position && devtools.setPosition(position) - errorTypes && devtools.setErrorTypes(errorTypes) - buttonPosition && devtools.setButtonPosition(buttonPosition) - initialIsOpen && devtools.setInitialIsOpen(initialIsOpen) - return - } else if (!shouldLoadTools) { - return - } - - el = document.body.appendChild(document.createElement('div')) - el.classList.add('tsqd-parent-container') - - import('@tanstack/query-devtools').then((queryDevtools) => { - devtools = new queryDevtools.TanstackQueryDevtools({ - ...options(), - client: getResolvedQueryClient(), - queryFlavor: 'Angular Query', - version: '5', - onlineManager, - }) - - el && devtools.mount(el) - - // Unmount the devtools on application destroy - destroyRef.onDestroy(destroyDevtools) - }) - }) - }, }, - ] - } - return queryFeature('DeveloperTools', providers) + }, + ] + return queryFeature(QueryFeatureKind.DeveloperTools, providers) } /** @@ -351,6 +288,7 @@ export function withDevtools( */ export type QueryFeatures = DeveloperToolsFeature | PersistQueryClientFeature -export const queryFeatures = ['DeveloperTools', 'PersistQueryClient'] as const - -export type QueryFeatureKind = (typeof queryFeatures)[number] +export const enum QueryFeatureKind { + DeveloperTools, + PersistQueryClient, +} diff --git a/packages/angular-query-experimental/src/util/is-dev-mode/is-dev-mode.ts b/packages/angular-query-experimental/src/util/is-dev-mode/is-dev-mode.ts deleted file mode 100644 index 5c18cfcf51..0000000000 --- a/packages/angular-query-experimental/src/util/is-dev-mode/is-dev-mode.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export for mocking in tests - -export { isDevMode } from '@angular/core' From 95409dfa37132329c632ea298598adb430b09c23 Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:07:13 +0200 Subject: [PATCH 2/3] normal enum --- packages/angular-query-experimental/src/providers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index 1726380ab8..aceca3e3cc 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -288,7 +288,7 @@ function createDevtoolsFeature(withDevtoolsFn?: () => DevtoolsOptions) { */ export type QueryFeatures = DeveloperToolsFeature | PersistQueryClientFeature -export const enum QueryFeatureKind { +export enum QueryFeatureKind { DeveloperTools, PersistQueryClient, } From 8ee4ba7f3b94e8610ae7e3d66759301fc5f31a74 Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:15:26 +0200 Subject: [PATCH 3/3] Enable DI via deps options --- docs/framework/angular/devtools.md | 15 ++- .../src/devtools-setup.ts | 14 ++- .../src/providers.ts | 93 ++++++++++++++----- 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/docs/framework/angular/devtools.md b/docs/framework/angular/devtools.md index b5149f6f3a..00f20b82d3 100644 --- a/docs/framework/angular/devtools.md +++ b/docs/framework/angular/devtools.md @@ -81,10 +81,15 @@ export const appConfig: ApplicationConfig = { provideHttpClient(), provideTanStackQuery( new QueryClient(), - withDevtools(() => ({ - initialIsOpen: true, - loadDevtools: inject(DevtoolsOptionsManager).loadDevtools(), - })), + withDevtools( + (devToolsOptionsManager: DevtoolsOptionsManager) => ({ + loadDevtools: devToolsOptionsManager.loadDevtools(), + }), + { + // `deps` can be used to pass one or more injectables as parameters to the `withDevtools` callback. + deps: [DevtoolsOptionsManager], + }, + ), ), ], } @@ -92,7 +97,7 @@ export const appConfig: ApplicationConfig = { ### Options -Of these options `client`, `position`, `errorTypes`, `buttonPosition`, and `initialIsOpen` support reactivity through signals. +Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosition`, and `initialIsOpen` support reactivity through signals. - `loadDevtools?: 'auto' | boolean` - Defaults to `auto`: lazily loads devtools when in development mode. Skips loading in production mode. diff --git a/packages/angular-query-experimental/src/devtools-setup.ts b/packages/angular-query-experimental/src/devtools-setup.ts index 0856bdbbe2..231535fca1 100644 --- a/packages/angular-query-experimental/src/devtools-setup.ts +++ b/packages/angular-query-experimental/src/devtools-setup.ts @@ -1,6 +1,6 @@ import { DestroyRef, computed, effect } from '@angular/core' import { QueryClient, onlineManager } from '@tanstack/query-core' -import type { Injector } from '@angular/core' +import type { Injector, Signal } from '@angular/core' import type { TanstackQueryDevtools } from '@tanstack/query-devtools' import type { DevtoolsOptions } from './providers' @@ -10,7 +10,7 @@ declare const ngDevMode: unknown // and to minimize bundle size export function setupDevtools( injector: Injector, - withDevtoolsFn?: () => DevtoolsOptions, + devtoolsOptions: Signal, ) { const isDevMode = typeof ngDevMode !== 'undefined' && ngDevMode const injectedClient = injector.get(QueryClient, { @@ -18,18 +18,16 @@ export function setupDevtools( }) const destroyRef = injector.get(DestroyRef) - const options = computed(() => withDevtoolsFn?.() ?? {}) - let devtools: TanstackQueryDevtools | null = null let el: HTMLElement | null = null const shouldLoadToolsSignal = computed(() => { - const { loadDevtools } = options() + const { loadDevtools } = devtoolsOptions() return typeof loadDevtools === 'boolean' ? loadDevtools : isDevMode }) const getResolvedQueryClient = () => { - const client = options().client ?? injectedClient + const client = devtoolsOptions().client ?? injectedClient if (!client) { throw new Error('No QueryClient found') } @@ -46,7 +44,7 @@ export function setupDevtools( () => { const shouldLoadTools = shouldLoadToolsSignal() const { client, position, errorTypes, buttonPosition, initialIsOpen } = - options() + devtoolsOptions() if (devtools && !shouldLoadTools) { destroyDevtools() @@ -67,7 +65,7 @@ export function setupDevtools( import('@tanstack/query-devtools').then((queryDevtools) => { devtools = new queryDevtools.TanstackQueryDevtools({ - ...options(), + ...devtoolsOptions(), client: getResolvedQueryClient(), queryFlavor: 'Angular Query', version: '5', diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index aceca3e3cc..c1b2f82b1f 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -1,15 +1,17 @@ import { DestroyRef, ENVIRONMENT_INITIALIZER, + InjectionToken, Injector, PLATFORM_ID, + computed, inject, makeEnvironmentProviders, } from '@angular/core' import { QueryClient } from '@tanstack/query-core' import { isPlatformBrowser } from '@angular/common' import { noop } from './util' -import type { EnvironmentProviders, Provider } from '@angular/core' +import type { EnvironmentProviders, Provider, Signal } from '@angular/core' import type { DevtoolsButtonPosition, DevtoolsErrorType, @@ -18,6 +20,13 @@ import type { declare const ngDevMode: unknown +/** + * @internal + */ +const DEVTOOLS_OPTIONS_SIGNAL = new InjectionToken>( + 'devtools options signal', +) + /** * Usually {@link provideTanStackQuery} is used once to set up TanStack Query and the * {@link https://tanstack.com/query/latest/docs/reference/QueryClient|QueryClient} @@ -163,6 +172,36 @@ export type DeveloperToolsFeature = export type PersistQueryClientFeature = QueryFeature +/** + * Options for configuring withDevtools. + * @public + */ +export interface WithDevtoolsOptions { + /** + * An array of dependencies to be injected and passed to the `withDevtoolsFn` function. + * + * **Example** + * ```ts + * export const appConfig: ApplicationConfig = { + * providers: [ + * provideTanStackQuery( + * new QueryClient(), + * withDevtools( + * (devToolsOptionsManager: DevtoolsOptionsManager) => ({ + * loadDevtools: devToolsOptionsManager.loadDevtools(), + * }), + * { + * deps: [DevtoolsOptionsManager], + * }, + * ), + * ), + * ], + * } + * ``` + */ + deps?: Array +} + /** * Options for configuring the TanStack Query devtools. * @public @@ -242,39 +281,47 @@ export interface DevtoolsOptions { * * If you need more control over where devtools are rendered, consider `injectDevtoolsPanel`. This allows rendering devtools inside your own devtools for example. * @param withDevtoolsFn - A function that returns `DevtoolsOptions`. + * @param options - Additional options for configuring `withDevtools`. * @returns A set of providers for use with `provideTanStackQuery`. * @public * @see {@link provideTanStackQuery} * @see {@link DevtoolsOptions} */ export function withDevtools( - withDevtoolsFn?: () => DevtoolsOptions, + withDevtoolsFn?: (...deps: Array) => DevtoolsOptions, + options: WithDevtoolsOptions = {}, ): DeveloperToolsFeature { + let providers: Array = [] if (withDevtoolsFn === undefined && typeof ngDevMode === 'undefined') { - return queryFeature(QueryFeatureKind.DeveloperTools, []) + return queryFeature(QueryFeatureKind.DeveloperTools, providers) } else { - return createDevtoolsFeature(withDevtoolsFn) - } -} - -function createDevtoolsFeature(withDevtoolsFn?: () => DevtoolsOptions) { - const providers = [ - { - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useFactory: () => { - if (!isPlatformBrowser(inject(PLATFORM_ID))) return noop - let destroyed = false - const injector = inject(Injector) - inject(DestroyRef).onDestroy(() => (destroyed = true)) + providers = [ + { + provide: DEVTOOLS_OPTIONS_SIGNAL, + useFactory: (...deps: Array) => + computed(() => withDevtoolsFn?.(...deps) ?? {}), + deps: options.deps || [], + }, + { + // Do not use provideEnvironmentInitializer while Angular < v19 is supported + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory: (devtoolsOptionsSignal: Signal) => { + if (!isPlatformBrowser(inject(PLATFORM_ID))) return noop + let destroyed = false + const injector = inject(Injector) + inject(DestroyRef).onDestroy(() => (destroyed = true)) - return () => - import('./devtools-setup').then((module) => { - !destroyed && module.setupDevtools(injector, withDevtoolsFn) - }) + return () => + import('./devtools-setup').then((module) => { + !destroyed && + module.setupDevtools(injector, devtoolsOptionsSignal) + }) + }, + deps: [DEVTOOLS_OPTIONS_SIGNAL], }, - }, - ] + ] + } return queryFeature(QueryFeatureKind.DeveloperTools, providers) }