Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/legal-lamps-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/query-core': patch
---

Avoid repeatedly setting up custom focus and online event listeners that do not return a cleanup function when subscribers are added.
37 changes: 37 additions & 0 deletions packages/query-core/src/__tests__/focusManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,43 @@ describe('focusManager', () => {
vi.useRealTimers()
})

it('should initialize a listener without cleanup once until the last unsubscribe', () => {
const setup = vi.fn<() => undefined>().mockReturnValue(undefined)

focusManager.setEventListener(setup)
const unsubscribe1 = focusManager.subscribe(() => undefined)
const unsubscribe2 = focusManager.subscribe(() => undefined)
expect(setup).toHaveBeenCalledTimes(1)

unsubscribe1()
expect(setup).toHaveBeenCalledTimes(1)
unsubscribe2()

const unsubscribe3 = focusManager.subscribe(() => undefined)
expect(setup).toHaveBeenCalledTimes(2)
unsubscribe3()
})

it('should notify each subscriber once per event when setup returns no cleanup', () => {
const source = new EventTarget()
const listener1 = vi.fn<(focused: boolean) => void>()
const listener2 = vi.fn<(focused: boolean) => void>()

focusManager.setEventListener((onFocus) => {
source.addEventListener('focus', () => onFocus())
return undefined
})
const unsubscribe1 = focusManager.subscribe(listener1)
const unsubscribe2 = focusManager.subscribe(listener2)

source.dispatchEvent(new Event('focus'))
expect(listener1).toHaveBeenCalledExactlyOnceWith(true)
expect(listener2).toHaveBeenCalledExactlyOnceWith(true)

unsubscribe1()
unsubscribe2()
})

it('should call previous remove handler when replacing an event listener', () => {
const remove1Spy = vi.fn<() => void>()
const remove2Spy = vi.fn<() => void>()
Expand Down
17 changes: 17 additions & 0 deletions packages/query-core/src/__tests__/onlineManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ describe('onlineManager', () => {
vi.useRealTimers()
})

it('should initialize a listener without cleanup once until the last unsubscribe', () => {
const setup = vi.fn<() => undefined>().mockReturnValue(undefined)

onlineManager.setEventListener(setup)
const unsubscribe1 = onlineManager.subscribe(() => undefined)
const unsubscribe2 = onlineManager.subscribe(() => undefined)
expect(setup).toHaveBeenCalledTimes(1)

unsubscribe1()
expect(setup).toHaveBeenCalledTimes(1)
unsubscribe2()

const unsubscribe3 = onlineManager.subscribe(() => undefined)
expect(setup).toHaveBeenCalledTimes(2)
unsubscribe3()
})

it('should return true from isOnline if navigator is undefined', () => {
const navigatorSpy = vi.spyOn(globalThis, 'navigator', 'get')

Expand Down
16 changes: 9 additions & 7 deletions packages/query-core/src/focusManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Subscribable } from './subscribable'
import { noop } from './utils'

type Listener = (focused: boolean) => void

Expand Down Expand Up @@ -78,13 +79,14 @@ export class FocusManager extends Subscribable<Listener> {
setEventListener(setup: SetupFn): void {
this.#setup = setup
this.#cleanup?.()
this.#cleanup = setup((focused) => {
if (typeof focused === 'boolean') {
this.setFocused(focused)
} else {
this.onFocus()
}
})
this.#cleanup =
setup((focused) => {
if (typeof focused === 'boolean') {
this.setFocused(focused)
} else {
this.onFocus()
}
}) ?? noop
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/query-core/src/onlineManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Subscribable } from './subscribable'
import { noop } from './utils'

type Listener = (online: boolean) => void
type SetupFn = (setOnline: Listener) => (() => void) | undefined
Expand Down Expand Up @@ -76,7 +77,7 @@ export class OnlineManager extends Subscribable<Listener> {
setEventListener(setup: SetupFn): void {
this.#setup = setup
this.#cleanup?.()
this.#cleanup = setup(this.setOnline.bind(this))
this.#cleanup = setup(this.setOnline.bind(this)) ?? noop
}

/**
Expand Down