Add ConnectionManager for robust React lifecycle handling#4028
Add ConnectionManager for robust React lifecycle handling#4028douglance wants to merge 5 commits intoclockworklabs:masterfrom
Conversation
Implements TanStack Query-style connection management: - ConnectionManager singleton with retain/release reference counting - useSyncExternalStore for React state subscriptions - Deferred cleanup handles StrictMode mount/unmount/remount cycle - Connection sharing across providers with same uri/moduleName key This replaces the setTimeout workaround in SpacetimeDBProvider with a proper architectural solution at the SDK layer.
- Add 33 unit tests covering reference counting, deferred cleanup, React StrictMode simulation, state management, and subscriptions - Add React Integration section to TypeScript reference docs with SpacetimeDBProvider, useSpacetimeDB, and useTable documentation - Add StrictMode compatibility note to SDK README - Add module-level JSDoc to connection_manager.ts explaining the TanStack Query-style pattern for handling React lifecycles
b797a77 to
da4edb3
Compare
|
Thank you for opening this and the detailed description! @cloutiertyler also tried to fix the StrictMode problem but did so by briefly delaying disconnection (#4017). What do you think of that approach? |
|
@bfops I was watching him work on that PR on stream. That's why I wanted to open this one. There are scenarios where that solution doesn't work. The timing is not guaranteed. This solution (basically just using a singleton outside of React), guarantees the system timing works as expected in all scenarios. It's a robust pattern used by the biggest libraries solving this problem. I would consider it the best solution for this problem. |
I don't believe that is correct. I believe the timing is guaranteed. Specifically, React will double render before JavaScript releases to the next event loop iteration. Am I missing something? |
|
@cloutiertyler Stepping back from React details: this is just a shared resource ownership problem. StrictMode causes a fast start → stop → start sequence. The “delay disconnect” fix works today because React currently does all of that before the event loop advances. But that’s an implementation detail, not a contract. Relying on a timeout is basically saying: “wait a moment and hope no one actually needed the resource”. That’s a timing hack. It breaks as soon as you have multiple consumers, nested lifetimes, manual disconnects, or future scheduler changes. The real issue is ownership. A WebSocket is a shared resource. It shouldn’t be owned by whichever component mounted last. Shared resources need explicit lifecycle management: reference counting, retain/release semantics, and a single owner outside the UI layer. The |
Summary
ConnectionManagersingleton that uses reference counting and deferred cleanup to handle React StrictMode's double-mount behaviorretain()/release()instead of direct connect/disconnectuseSyncExternalStorefor tear-free state reads in ReactChanges
src/sdk/connection_manager.ts- Core ConnectionManager implementationsrc/react/SpacetimeDBProvider.ts- Now uses ConnectionManagersrc/react/connection_state.ts- Simplified type, imports from ConnectionManagersrc/sdk/db_connection_builder.ts- AddedgetUri()andgetModuleName()methodssrc/sdk/db_connection_impl.ts- Minor type updates for ConnectionManager integrationtests/connection_manager.test.ts- 33 unit testsTest plan
pnpm testpasses (104 tests)pnpm buildpassesMotivation
The Problem
React StrictMode double-mounts components in development to help catch bugs:
Without ConnectionManager, you get either:
Why ConnectionManager? (vs alternatives)
useSyncExternalStoreConnectionManager approach (TanStack Query pattern):
setTimeout(0)lets StrictMode remount cancel cleanupWhy Two
useSyncExternalStoreUsages?The SDK now has two places using
useSyncExternalStore. They solve different problems:useTableConnectionManagerDbConnection.db.tableNamecallbacksConnectionManagersingletonisActive,identity,errorchangeuseTableassumes a connection existsConnectionManagermanages WHEN the connection existsThe Layered Architecture
useTablesyncs data from an existing connectionConnectionManagersyncs connection state AND manages when the connection exists