react-realtime-hooks 1.4.1 → 1.4.2
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 +14 -0
- package/dist/index.cjs +12 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ Real apps need:
|
|
|
38
38
|
- Discriminated connection snapshots: `idle`, `connecting`, `open`, `reconnecting`, `closing`, `closed`, `error`.
|
|
39
39
|
- First-class TypeScript support with generic message types and custom parsers/serializers.
|
|
40
40
|
- SSR-safe by default. No browser-only globals are touched during server render.
|
|
41
|
+
- Strict Mode safe. Works correctly under React 18+ `<StrictMode>` and Next.js dev double-mount — see [Strict Mode Safety](#strict-mode-safety).
|
|
41
42
|
- Zero runtime dependencies beyond React.
|
|
42
43
|
- Manual controls stay available when you need them: `open()`, `close()`, `reconnect()`, `send()`.
|
|
43
44
|
|
|
@@ -655,6 +656,19 @@ That keeps the gate predictable when multiple blockers apply at once.
|
|
|
655
656
|
- No transport polyfills are bundled. Provide your own runtime support where needed.
|
|
656
657
|
- Browser-native transport constraints still apply: auth, proxy, CORS, and network policy are outside the hook's control.
|
|
657
658
|
|
|
659
|
+
## Strict Mode Safety
|
|
660
|
+
|
|
661
|
+
All hooks are safe to use under React 18+ `<React.StrictMode>` and the Next.js App Router's dev mode, both of which intentionally mount → unmount → mount each component on first render to surface effect-cleanup bugs.
|
|
662
|
+
|
|
663
|
+
Concretely:
|
|
664
|
+
|
|
665
|
+
- `useWebSocket` and `useEventSource` defer the actual `new WebSocket(...)` / `new EventSource(...)` call by a microtask. If the component unmounts before that microtask runs (the Strict Mode discard mount), no transport is ever created. If the mount survives, exactly one transport is created — never two.
|
|
666
|
+
- All effects in the library tear down their timers, listeners, and transports synchronously in the cleanup function. There is no "zombie" `setInterval`, no orphaned `addEventListener`, no leaked `WebSocket` left in `CONNECTING` after a discarded mount.
|
|
667
|
+
- Latest-state refs are committed via `useInsertionEffect`, not by writing to `ref.current` during render. A discarded render never leaves the ref out of sync with the committed tree.
|
|
668
|
+
- The library's own test suite runs every hook test inside `<React.StrictMode>` by default, so any regression that only shows up under double-mount is caught in CI.
|
|
669
|
+
|
|
670
|
+
If you observe a Strict Mode regression — e.g. two simultaneous WebSocket connections, an `EventSource` that survives a closed component, or a heartbeat that keeps firing after unmount — please open an issue with a minimal repro. That class of bug is supposed to be impossible by construction, and we treat it as a correctness defect.
|
|
671
|
+
|
|
658
672
|
## Testing And Quality
|
|
659
673
|
|
|
660
674
|
The package includes behavior tests for:
|
package/dist/index.cjs
CHANGED
|
@@ -466,7 +466,9 @@ var useReconnect = (options = {}) => {
|
|
|
466
466
|
() => createInitialState(normalizedOptions.enabled)
|
|
467
467
|
);
|
|
468
468
|
const stateRef = react.useRef(state);
|
|
469
|
-
|
|
469
|
+
react.useInsertionEffect(() => {
|
|
470
|
+
stateRef.current = state;
|
|
471
|
+
});
|
|
470
472
|
const commitState = (next) => {
|
|
471
473
|
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
472
474
|
stateRef.current = resolved;
|
|
@@ -599,7 +601,9 @@ var useHeartbeat = (options) => {
|
|
|
599
601
|
() => createInitialState2(enabled && startOnMount)
|
|
600
602
|
);
|
|
601
603
|
const stateRef = react.useRef(state);
|
|
602
|
-
|
|
604
|
+
react.useInsertionEffect(() => {
|
|
605
|
+
stateRef.current = state;
|
|
606
|
+
});
|
|
603
607
|
const commitState = (next) => {
|
|
604
608
|
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
605
609
|
stateRef.current = resolved;
|
|
@@ -942,7 +946,9 @@ var useWebSocket = (options) => {
|
|
|
942
946
|
() => createInitialState4(connect ? "connecting" : "idle")
|
|
943
947
|
);
|
|
944
948
|
const stateRef = react.useRef(state);
|
|
945
|
-
|
|
949
|
+
react.useInsertionEffect(() => {
|
|
950
|
+
stateRef.current = state;
|
|
951
|
+
});
|
|
946
952
|
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
947
953
|
const reconnect = useReconnect(
|
|
948
954
|
options.reconnect === false ? { enabled: false } : {
|
|
@@ -1515,7 +1521,9 @@ var useEventSource = (options) => {
|
|
|
1515
1521
|
() => createInitialState5(connect ? "connecting" : "idle")
|
|
1516
1522
|
);
|
|
1517
1523
|
const stateRef = react.useRef(state);
|
|
1518
|
-
|
|
1524
|
+
react.useInsertionEffect(() => {
|
|
1525
|
+
stateRef.current = state;
|
|
1526
|
+
});
|
|
1519
1527
|
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
1520
1528
|
const reconnect = useReconnect(
|
|
1521
1529
|
options.reconnect === false ? { enabled: false } : {
|