react-realtime-hooks 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -9
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +66 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://www.npmjs.com/package/react)
|
|
9
9
|
|
|
10
|
-
Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness.
|
|
10
|
+
Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness including page visibility.
|
|
11
11
|
|
|
12
|
-
`react-realtime-hooks` is for apps that need more than "open a socket and hope for the best". It gives you composable hooks for transport lifecycle, retry strategy, heartbeat,
|
|
12
|
+
`react-realtime-hooks` is for apps that need more than "open a socket and hope for the best". It gives you composable hooks for transport lifecycle, retry strategy, heartbeat, online status, and page visibility, so your UI can react to realtime state without rebuilding the same connection logic in every screen.
|
|
13
13
|
|
|
14
14
|
Live demo: https://volkov85.github.io/react-realtime-hooks/
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ Real apps need:
|
|
|
23
23
|
- reconnect strategy with caps, jitter, and manual control
|
|
24
24
|
- heartbeat and timeout tracking
|
|
25
25
|
- clean SSR behavior
|
|
26
|
-
- browser network awareness
|
|
26
|
+
- browser network and page visibility awareness
|
|
27
27
|
- typed message parsing and sending
|
|
28
28
|
|
|
29
29
|
`react-realtime-hooks` packages those concerns into small hooks that compose cleanly in React.
|
|
@@ -46,7 +46,7 @@ Real apps need:
|
|
|
46
46
|
| Connection state | You model it yourself | Built-in status model you can render directly |
|
|
47
47
|
| Reconnect flow | Manual timers and teardown | `useReconnect` with backoff, jitter, and limits |
|
|
48
48
|
| Heartbeat | Custom ping/pong loop | `heartbeat` support with timeout and latency |
|
|
49
|
-
|
|
|
49
|
+
| Browser awareness | Separate browser event wiring | `useOnlineStatus` and `usePageVisibility` for browser state |
|
|
50
50
|
| SSR safety | Easy to break during render | Browser-only behavior stays out of server render |
|
|
51
51
|
| UI ergonomics | Event handlers and refs everywhere | Hook result already shaped for product UI |
|
|
52
52
|
|
|
@@ -65,7 +65,11 @@ Peer dependency:
|
|
|
65
65
|
## How It Feels
|
|
66
66
|
|
|
67
67
|
```tsx
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
useOnlineStatus,
|
|
70
|
+
usePageVisibility,
|
|
71
|
+
useWebSocket
|
|
72
|
+
} from "react-realtime-hooks";
|
|
69
73
|
|
|
70
74
|
type IncomingMessage =
|
|
71
75
|
| { type: "notification"; text: string }
|
|
@@ -75,6 +79,7 @@ type OutgoingMessage = { type: "ack"; id: string } | { type: "ping" };
|
|
|
75
79
|
|
|
76
80
|
export function NotificationsPanel() {
|
|
77
81
|
const network = useOnlineStatus();
|
|
82
|
+
const page = usePageVisibility();
|
|
78
83
|
const socket = useWebSocket<IncomingMessage, OutgoingMessage>({
|
|
79
84
|
url: "ws://localhost:8080/notifications",
|
|
80
85
|
parseMessage: (event) => JSON.parse(String(event.data)) as IncomingMessage,
|
|
@@ -93,8 +98,8 @@ export function NotificationsPanel() {
|
|
|
93
98
|
return (
|
|
94
99
|
<section>
|
|
95
100
|
<p>
|
|
96
|
-
|
|
97
|
-
{socket.status}
|
|
101
|
+
Page: {page.isVisible ? "visible" : "hidden"} | Network:{" "}
|
|
102
|
+
{network.isOnline ? "online" : "offline"} | Transport: {socket.status}
|
|
98
103
|
</p>
|
|
99
104
|
|
|
100
105
|
{socket.status === "reconnecting" && (
|
|
@@ -146,7 +151,7 @@ Browser APIs
|
|
|
146
151
|
WebSocket / EventSource / navigator.onLine
|
|
147
152
|
|
|
148
153
|
Core hooks
|
|
149
|
-
useReconnect / useHeartbeat / useOnlineStatus
|
|
154
|
+
useReconnect / useHeartbeat / useOnlineStatus / usePageVisibility
|
|
150
155
|
|
|
151
156
|
Transport hooks
|
|
152
157
|
useWebSocket / useEventSource
|
|
@@ -205,6 +210,7 @@ This library already models those edges in a reusable way.
|
|
|
205
210
|
| `useReconnect` | Reusable retry and backoff logic | `schedule()`, `cancel()`, `reset()`, `attempt`, `status` |
|
|
206
211
|
| `useHeartbeat` | Liveness checks and timeout tracking | `start()`, `stop()`, `beat()`, `notifyAck()`, `latencyMs` |
|
|
207
212
|
| `useOnlineStatus` | Browser online/offline state | `isOnline`, `isSupported`, transition timestamps |
|
|
213
|
+
| `usePageVisibility` | Browser tab/page visibility state | `isVisible`, `visibilityState`, `isSupported`, transition timestamps |
|
|
208
214
|
|
|
209
215
|
## Transport Examples
|
|
210
216
|
|
|
@@ -334,6 +340,23 @@ export function NetworkIndicator() {
|
|
|
334
340
|
}
|
|
335
341
|
```
|
|
336
342
|
|
|
343
|
+
### `usePageVisibility`
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { usePageVisibility } from "react-realtime-hooks";
|
|
347
|
+
|
|
348
|
+
export function AttentionAwareBadge() {
|
|
349
|
+
const page = usePageVisibility({
|
|
350
|
+
trackTransitions: true,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<span>
|
|
355
|
+
{page.isVisible ? "Active tab" : "Background tab"} ({page.visibilityState})
|
|
356
|
+
</span>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
337
360
|
## API Reference
|
|
338
361
|
|
|
339
362
|
<details>
|
|
@@ -508,6 +531,28 @@ When you configure `useWebSocket` heartbeat, you can also set `timeoutAction` an
|
|
|
508
531
|
|
|
509
532
|
</details>
|
|
510
533
|
|
|
534
|
+
<details>
|
|
535
|
+
<summary><strong>usePageVisibility</strong></summary>
|
|
536
|
+
|
|
537
|
+
### Options
|
|
538
|
+
|
|
539
|
+
| Option | Type | Default | Description |
|
|
540
|
+
| ------------------ | --------- | ------- | ----------------------------------------------------------- |
|
|
541
|
+
| `initialVisible` | `boolean` | `true` | Fallback value when the Visibility API is unavailable |
|
|
542
|
+
| `trackTransitions` | `boolean` | `true` | Tracks `lastChangedAt`, `becameVisibleAt`, `becameHiddenAt` |
|
|
543
|
+
|
|
544
|
+
### Result
|
|
545
|
+
|
|
546
|
+
| Field | Type | Description |
|
|
547
|
+
| ----------------- | ------------------------------------ | ------------------------------------------------ |
|
|
548
|
+
| `isVisible` | `boolean` | Whether the current page is visible |
|
|
549
|
+
| `visibilityState` | `DocumentVisibilityState \| "visible"` | Current browser visibility state |
|
|
550
|
+
| `isSupported` | `boolean` | Whether `document.visibilityState` is available |
|
|
551
|
+
| `lastChangedAt` | `number \| null` | Timestamp of the last visibility transition |
|
|
552
|
+
| `becameVisibleAt` | `number \| null` | Timestamp of the last visible transition |
|
|
553
|
+
| `becameHiddenAt` | `number \| null` | Timestamp of the last hidden transition |
|
|
554
|
+
|
|
555
|
+
</details>
|
|
511
556
|
## Limitations And Edge Cases
|
|
512
557
|
|
|
513
558
|
- `useEventSource` is receive-only by design. SSE is not a bidirectional transport.
|
|
@@ -527,7 +572,7 @@ The package includes behavior tests for:
|
|
|
527
572
|
- exponential backoff
|
|
528
573
|
- timer and listener cleanup
|
|
529
574
|
- heartbeat start / stop / timeout
|
|
530
|
-
- browser offline / online transitions
|
|
575
|
+
- browser offline / online and page visibility transitions
|
|
531
576
|
- invalid payload and parse errors
|
|
532
577
|
- manual reconnect and manual close
|
|
533
578
|
|
|
@@ -551,3 +596,7 @@ Development and release workflow live in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
|
551
596
|
## License
|
|
552
597
|
|
|
553
598
|
MIT
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var react = require('react');
|
|
|
8
8
|
var isWebSocketSupported = () => typeof WebSocket !== "undefined";
|
|
9
9
|
var isEventSourceSupported = () => typeof EventSource !== "undefined";
|
|
10
10
|
var hasNavigatorOnLineSupport = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean";
|
|
11
|
+
var hasDocumentVisibilitySupport = () => typeof document !== "undefined" && typeof document.visibilityState === "string";
|
|
11
12
|
var readOnlineStatus = (initialOnline = true) => {
|
|
12
13
|
if (!hasNavigatorOnLineSupport()) {
|
|
13
14
|
return {
|
|
@@ -20,6 +21,20 @@ var readOnlineStatus = (initialOnline = true) => {
|
|
|
20
21
|
isSupported: true
|
|
21
22
|
};
|
|
22
23
|
};
|
|
24
|
+
var readPageVisibility = (initialVisible = true) => {
|
|
25
|
+
if (!hasDocumentVisibilitySupport()) {
|
|
26
|
+
return {
|
|
27
|
+
isVisible: initialVisible,
|
|
28
|
+
isSupported: false,
|
|
29
|
+
visibilityState: initialVisible ? "visible" : "hidden"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
isVisible: document.visibilityState === "visible",
|
|
34
|
+
isSupported: true,
|
|
35
|
+
visibilityState: document.visibilityState
|
|
36
|
+
};
|
|
37
|
+
};
|
|
23
38
|
|
|
24
39
|
// src/hooks/useOnlineStatus.ts
|
|
25
40
|
var subscribeToOnlineStatus = (onStoreChange) => {
|
|
@@ -72,6 +87,56 @@ var useOnlineStatus = (options = {}) => {
|
|
|
72
87
|
...transitions
|
|
73
88
|
};
|
|
74
89
|
};
|
|
90
|
+
var subscribeToPageVisibility = (onStoreChange) => {
|
|
91
|
+
if (typeof document === "undefined" || !hasDocumentVisibilitySupport()) {
|
|
92
|
+
return () => {
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
document.addEventListener("visibilitychange", onStoreChange);
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener("visibilitychange", onStoreChange);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
var createEmptyTransitionState2 = () => ({
|
|
101
|
+
lastChangedAt: null,
|
|
102
|
+
becameHiddenAt: null,
|
|
103
|
+
becameVisibleAt: null
|
|
104
|
+
});
|
|
105
|
+
var usePageVisibility = (options = {}) => {
|
|
106
|
+
const initialVisible = options.initialVisible ?? true;
|
|
107
|
+
const trackTransitions = options.trackTransitions ?? true;
|
|
108
|
+
const visibilityState = react.useSyncExternalStore(
|
|
109
|
+
subscribeToPageVisibility,
|
|
110
|
+
() => readPageVisibility(initialVisible).visibilityState,
|
|
111
|
+
() => initialVisible ? "visible" : "hidden"
|
|
112
|
+
);
|
|
113
|
+
const isVisible = visibilityState === "visible";
|
|
114
|
+
const previousVisibleRef = react.useRef(isVisible);
|
|
115
|
+
const [transitions, setTransitions] = react.useState(createEmptyTransitionState2);
|
|
116
|
+
react.useEffect(() => {
|
|
117
|
+
if (!trackTransitions) {
|
|
118
|
+
previousVisibleRef.current = isVisible;
|
|
119
|
+
setTransitions(createEmptyTransitionState2);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (previousVisibleRef.current === isVisible) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const changedAt = Date.now();
|
|
126
|
+
previousVisibleRef.current = isVisible;
|
|
127
|
+
setTransitions((current) => ({
|
|
128
|
+
lastChangedAt: changedAt,
|
|
129
|
+
becameHiddenAt: isVisible ? current.becameHiddenAt : changedAt,
|
|
130
|
+
becameVisibleAt: isVisible ? changedAt : current.becameVisibleAt
|
|
131
|
+
}));
|
|
132
|
+
}, [isVisible, trackTransitions]);
|
|
133
|
+
return {
|
|
134
|
+
isSupported: hasDocumentVisibilitySupport(),
|
|
135
|
+
isVisible,
|
|
136
|
+
visibilityState,
|
|
137
|
+
...transitions
|
|
138
|
+
};
|
|
139
|
+
};
|
|
75
140
|
|
|
76
141
|
// src/core/reconnect.ts
|
|
77
142
|
var DEFAULT_RECONNECT_OPTIONS = {
|
|
@@ -1398,6 +1463,7 @@ var useEventSource = (options) => {
|
|
|
1398
1463
|
exports.useEventSource = useEventSource;
|
|
1399
1464
|
exports.useHeartbeat = useHeartbeat;
|
|
1400
1465
|
exports.useOnlineStatus = useOnlineStatus;
|
|
1466
|
+
exports.usePageVisibility = usePageVisibility;
|
|
1401
1467
|
exports.useReconnect = useReconnect;
|
|
1402
1468
|
exports.useWebSocket = useWebSocket;
|
|
1403
1469
|
//# sourceMappingURL=index.cjs.map
|