react-realtime-hooks 1.4.1 → 1.4.3
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 +16 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +17 -7
- 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;
|
|
@@ -938,11 +942,14 @@ var useWebSocket = (options) => {
|
|
|
938
942
|
const nextSocketEpochRef = react.useRef(0);
|
|
939
943
|
const controller = useWebSocketController();
|
|
940
944
|
const [openNonce, setOpenNonce] = react.useState(0);
|
|
945
|
+
const initialStatus = !supported || resolvedUrl === null ? "closed" : connect ? "connecting" : "idle";
|
|
941
946
|
const [state, setState] = react.useState(
|
|
942
|
-
() => createInitialState4(
|
|
947
|
+
() => createInitialState4(initialStatus)
|
|
943
948
|
);
|
|
944
949
|
const stateRef = react.useRef(state);
|
|
945
|
-
|
|
950
|
+
react.useInsertionEffect(() => {
|
|
951
|
+
stateRef.current = state;
|
|
952
|
+
});
|
|
946
953
|
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
947
954
|
const reconnect = useReconnect(
|
|
948
955
|
options.reconnect === false ? { enabled: false } : {
|
|
@@ -1511,11 +1518,14 @@ var useEventSource = (options) => {
|
|
|
1511
1518
|
const suppressReconnectRef = react.useRef(false);
|
|
1512
1519
|
const terminalErrorRef = react.useRef(null);
|
|
1513
1520
|
const [openNonce, setOpenNonce] = react.useState(0);
|
|
1521
|
+
const initialStatus = !supported || resolvedUrl === null ? "closed" : connect ? "connecting" : "idle";
|
|
1514
1522
|
const [state, setState] = react.useState(
|
|
1515
|
-
() => createInitialState5(
|
|
1523
|
+
() => createInitialState5(initialStatus)
|
|
1516
1524
|
);
|
|
1517
1525
|
const stateRef = react.useRef(state);
|
|
1518
|
-
|
|
1526
|
+
react.useInsertionEffect(() => {
|
|
1527
|
+
stateRef.current = state;
|
|
1528
|
+
});
|
|
1519
1529
|
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
1520
1530
|
const reconnect = useReconnect(
|
|
1521
1531
|
options.reconnect === false ? { enabled: false } : {
|