-
Notifications
You must be signed in to change notification settings - Fork 671
Add ConnectionManager for robust React lifecycle handling #4028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
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