Repository: TanStack/query (ts)

Issue:
OnlineManager's setOnline doesn't seem to work with subscriptions correctly
### Describe the bug

I've got a custom React hook for subscribing to changes in `onlineManager.isOnline`:

```typescript
import { useState, useEffect } from "react";
import { onlineManager } from "@tanstack/react-query";

export default function useIsOnline() {
  const [isOnline, setIsOnline] = useState(onlineManager.isOnline());

  useEffect(() => {
    const unsubscribe = onlineManager.subscribe(() => {
      console.log(`Online: ${onlineManager.isOnline()}`);
      setIsOnline(onlineManager.isOnline());
    });

    return () => unsubscribe();
  }, []);

  return isOnline;
}
```

It works great when default behavior is in place. But instead of relying on browser's `online`/`offline` events I've created a hook with `networkMode: 'always'` that periodically checks connection to API and depending on success/error of the request I manually set online state using `onlineManager.setOnline`.

The problem is `onlineManager.setOnline(false)` doesn't trigger callback in the subscription or maybe I am a bit misunderstanding the concept of `OnlineManager`.

### Your minimal, reproducible example

https://codesandbox.io/s/focused-williamson-gxks92

### Steps to reproduce

1. Set up subscription through `onlineManager.subscribe`.
2. Manually call `onlineManager.setOnline(false)`.

### Expected behavior

Expect the callback to be executed on both `onlineManager.setOnline(true)` and `onlineManager.setOnline(false)`.

### How often does this bug happen?

Every time

### Screenshots or Videos

_No response_

### Platform

- OS: Windows.
- Browser: Any modern one.

### Tanstack Query adapter

react-query

### TanStack Query version

4.28.0

### TypeScript version

4.9.4

### Additional context

_No response_

Pull request:
fix(core): onlineManager and focusManager should invoke listeners when a change occurred
This is only relevant when a custom online / focus boolean has been set; in those cases, we previously only informed listeners if the value was truthy, but not if it changed back to false or undefined again

With this fix, the managers inform the listeners whenever something has changed, regardless of which direction the change was

fixes #5674 

Patch:
diff --git a/packages/query-core/src/focusManager.ts b/packages/query-core/src/focusManager.ts
index 710a009c7..f2d45e6a8 100644
--- a/packages/query-core/src/focusManager.ts
+++ b/packages/query-core/src/focusManager.ts
@@ -58,9 +58,9 @@ export class FocusManager extends Subscribable {
   }
 
   setFocused(focused?: boolean): void {
-    this.focused = focused
-
-    if (focused) {
+    const changed = this.focused !== focused
+    if (changed) {
+      this.focused = focused
       this.onFocus()
     }
   }
diff --git a/packages/query-core/src/onlineManager.ts b/packages/query-core/src/onlineManager.ts
index c42a948a0..f7049f40d 100644
--- a/packages/query-core/src/onlineManager.ts
+++ b/packages/query-core/src/onlineManager.ts
@@ -63,9 +63,10 @@ export class OnlineManager extends Subscribable {
   }
 
   setOnline(online?: boolean): void {
-    this.online = online
+    const changed = this.online !== online
 
-    if (online) {
+    if (changed) {
+      this.online = online
       this.onOnline()
     }
   }


Test patch:
diff --git a/packages/query-core/src/tests/focusManager.test.tsx b/packages/query-core/src/tests/focusManager.test.tsx
index 645a93f47..6e23df625 100644
--- a/packages/query-core/src/tests/focusManager.test.tsx
+++ b/packages/query-core/src/tests/focusManager.test.tsx
@@ -38,20 +38,6 @@ describe('focusManager', () => {
     expect(focusManager.isFocused()).toBeTruthy()
   })
 
-  it('should not notify listeners on focus if already focused', async () => {
-    const subscriptionSpy = jest.fn()
-    const unsubscribe = focusManager.subscribe(subscriptionSpy)
-
-    focusManager.setFocused(true)
-    expect(subscriptionSpy).toHaveBeenCalledTimes(1)
-    subscriptionSpy.mockReset()
-
-    focusManager.setFocused(false)
-    expect(subscriptionSpy).toHaveBeenCalledTimes(0)
-
-    unsubscribe()
-  })
-
   it('should return true for isFocused if document is undefined', async () => {
     const { document } = globalThis
 
@@ -152,4 +138,25 @@ describe('focusManager', () => {
 
     unsubscribe2()
   })
+
+  test('should call listeners when setFocused is called', () => {
+    const listener = jest.fn()
+
+    focusManager.subscribe(listener)
+
+    focusManager.setFocused(true)
+    focusManager.setFocused(true)
+
+    expect(listener).toHaveBeenCalledTimes(1)
+
+    focusManager.setFocused(false)
+    focusManager.setFocused(false)
+
+    expect(listener).toHaveBeenCalledTimes(2)
+
+    focusManager.setFocused(undefined)
+    focusManager.setFocused(undefined)
+
+    expect(listener).toHaveBeenCalledTimes(3)
+  })
 })
diff --git a/packages/query-core/src/tests/onlineManager.test.tsx b/packages/query-core/src/tests/onlineManager.test.tsx
index c3a00e86d..ab35a7bbf 100644
--- a/packages/query-core/src/tests/onlineManager.test.tsx
+++ b/packages/query-core/src/tests/onlineManager.test.tsx
@@ -145,4 +145,25 @@ describe('onlineManager', () => {
 
     unsubscribe2()
   })
+
+  test('should call listeners when setOnline is called', () => {
+    const listener = jest.fn()
+
+    onlineManager.subscribe(listener)
+
+    onlineManager.setOnline(true)
+    onlineManager.setOnline(true)
+
+    expect(listener).toHaveBeenCalledTimes(1)
+
+    onlineManager.setOnline(false)
+    onlineManager.setOnline(false)
+
+    expect(listener).toHaveBeenCalledTimes(2)
+
+    onlineManager.setOnline(undefined)
+    onlineManager.setOnline(undefined)
+
+    expect(listener).toHaveBeenCalledTimes(3)
+  })
 })