Skip to content

refactor(angular-query): improve devtools tree shaking #9022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions docs/framework/angular/devtools.md
Original file line number Diff line number Diff line change
@@ -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<KeyboardEvent>(document, 'keydown').pipe(
map(
@@ -81,18 +81,23 @@ 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],
},
),
),
],
}
```

### 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.
155 changes: 110 additions & 45 deletions packages/angular-query-experimental/src/__tests__/providers.test.ts
Original file line number Diff line number Diff line change
@@ -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<DevtoolsErrorType>)
83 changes: 83 additions & 0 deletions packages/angular-query-experimental/src/devtools-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { DestroyRef, computed, effect } from '@angular/core'
import { QueryClient, onlineManager } from '@tanstack/query-core'
import type { Injector, Signal } 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,
devtoolsOptions: Signal<DevtoolsOptions>,
) {
const isDevMode = typeof ngDevMode !== 'undefined' && ngDevMode
const injectedClient = injector.get(QueryClient, {
optional: true,
})
const destroyRef = injector.get(DestroyRef)

let devtools: TanstackQueryDevtools | null = null
let el: HTMLElement | null = null

const shouldLoadToolsSignal = computed(() => {
const { loadDevtools } = devtoolsOptions()
return typeof loadDevtools === 'boolean' ? loadDevtools : isDevMode
})

const getResolvedQueryClient = () => {
const client = devtoolsOptions().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 } =
devtoolsOptions()

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({
...devtoolsOptions(),
client: getResolvedQueryClient(),
queryFlavor: 'Angular Query',
version: '5',
onlineManager,
})

el && devtools.mount(el)

// Unmount the devtools on application destroy
destroyRef.onDestroy(destroyDevtools)
})
},
{ injector },
)
}
Loading