**entanglement-react** *** # Entanglement React architecture This package treats Entanglement as an external mutable store and React as a snapshot consumer. That framing is the central architectural decision in the plugin. `PersistentSynchronizable` instances are long-lived shared objects. Entanglement mutates them in place when messages arrive, and `PersistentSynchronizable.syncConstruct()` reuses the same object for a given storage key rather than allocating a fresh instance on every update. Likewise, relationship getters resolve through the shared `syncStorageMap`, so traversing a relationship gives the current master object, not a detached copy. React does best when render inputs are immutable snapshots whose identity changes when the rendered value changes. The plugin bridges two different models: - Entanglement side: stable live objects with in-place mutation and event streams. - React side: immutable render values with explicit subscription boundaries. The rest of this document defines those snapshot semantics and the public API shape. # Concepts ## Mutability `PersistentSynchronizable` objects are mutable shared records stored in a class-level `syncStorageMap`. When a new message arrives, `syncReceive()` copies fields onto the existing object instance. That is great for synchronization, but it is exactly the opposite of what React components want to render directly. Consequences for React: 1. Do not put live Entanglement objects directly into React state and expect identity-based change detection to work. - the object reference is usually stable - fields on that object change underneath the component - `Object.is(previousObject, nextObject)` will often stay `true` even though the data changed 2. A list of live objects is not itself enough to describe render state. - membership can change - ordering can change - any member can mutate in place without the array identity changing unless the hook deliberately replaces the array snapshot 3. Rendering should happen from hook-managed values, not from ad hoc reads of `syncStorageMap` during render. - React needs a subscription boundary - the plugin owns that boundary The plugin exposes two categories of values: - live objects, when the caller explicitly wants to work with the Entanglement master copy - immutable snapshots, when the caller wants React-safe render input That distinction is visible in the docs and the API names. ## Snapshots A snapshot is the React-facing value produced from a live Entanglement object at a moment in time. Snapshot requirements: 1. It must not be the same object as the live master copy. 2. Callers treat it as immutable. 3. Its identity should change when the rendered contents change. 4. It is shallow and cheap. Entanglement already has the right conceptual primitive in `Synchronizable.syncClone()`, which creates a detached copy by round-tripping through `toSync()` and `syncReceive()`. The hook semantics match that model: render from a detached copy, not from the shared storage instance. Why this matters for React: - React render logic is easier to reason about when props do not mutate after render starts. - memoization works much better when snapshots are immutable values. - debugging becomes simpler because a snapshot seen in one render does not silently change before the next render. Important limitation: a snapshot is a boundary, not a deep frozen graph. If the root object has a relationship property implemented as a getter, traversing that relationship leaves the snapshot world and re-enters the live Entanglement world unless the caller explicitly snapshots that related object too. ## Relationships as snapshot boundary Relationship accessors in Entanglement resolve through the relevant class storage map each time they are read. That means `device.vmImage` or `vmImage.devices` are live lookups, not part of the root object snapshot. This is a feature, not a bug, as long as the boundary is explicit. Recommended rule: - if a component needs to render a related object, it should call another hook for that object or derive the relationship from a live object using `useEntangledValue`. Example: ```tsx function DeviceRow({ device }: { device: Device }) { const deviceSnapshot = useEntangledObject(device); const imageName = useEntangledValue(device, live => live.vmImage?.name ?? 'No image', [device]); if (!deviceSnapshot) return null; return ( {deviceSnapshot.name} {deviceSnapshot.type} {imageName} ); } ``` In that example the component renders a snapshot for the root record, but deliberately uses a selector for the related live object. # APIs Supporting types: ```tses export type EntangledClass = { new (...args: never[]): T; readonly syncType: string; readonly syncPrimaryKeys: readonly string[]; readonly syncStorageMap: ReadonlyMap; addEventListener(event: EntanglementObjectEvent, handler: (object: T) => void): void; removeEventListener(event: EntanglementObjectEvent, handler: (object: T) => void): void; }; export type EntangledSnapshot = Readonly<{ [K in keyof T]: T[K]; }>; export type EntangledStorageMap = ReadonlyMap; ``` `EntangledSnapshot` is intentionally shallow. That matches the architectural rule that relationships are snapshot boundaries. ## `useEntangledList` ```ts export interface UseEntangledListOptions { filter?: (object: T) => boolean; orderBy?: (left: T, right: T) => number; includeTransitions?: boolean; deps?: readonly unknown[]; } export function useEntangledList( target: EntangledClass, options?: UseEntangledListOptions, ): readonly T[]; ``` Returns a snapshot of list membership for all live objects of a given Entanglement type. More precisely: - the returned array is a React snapshot - the array identity changes when membership changes - the array items are still live `PersistentSynchronizable` objects - The orderBy function only re-sorts the list when membership changes. Only sort on keys that are not expected to change; if you need to sort on mutable attributes, do so outside of useEntangledList. - item field mutation can therefore happen without the item identity changing That last point is the main caveat: `useEntangledList()` is a list-membership hook, not an object-snapshot hook. Use it when: - you need the current set of objects of a given type - you want to filter or sort a collection - each row or child component will subscribe to its own object data separately Do not use it as your only hook if the component renders object fields directly and expects React to re-render when those fields change. Example: ```tsx function DeviceTable() { const devices = useEntangledList(Device, { filter: device => !device.pending, orderBy: (left, right) => left.name.localeCompare(right.name), deps: [], }); return ( {devices.map(device => ( ))} ); } ``` Caveats: - `filter` runs against live objects. - because objects are live, child components should usually call `useEntangledObject(device)` or `useEntangledValue(device, selector)`. - if ordering depends on mutable fields, the hook must recompute ordering on relevant object events, not just on create/delete. - if the filter or order functions change reference on each render, provide `deps` to prevent the filter from being rebuilt unnecessarily. Failure to do so will result in an infinite loop, not just excessive re-renders. ## `useEntangledObject` ```ts export function useEntangledObject( object: T | null | undefined, ): EntangledSnapshot | undefined; ``` Returns a React-safe snapshot of a single Entanglement object. Behavior: - when `object` is `null` or `undefined`, return `undefined` - when the object changes, return a new snapshot - when the object disappears, the hook returns `undefined` on the next render or otherwise reflects that the object is gone This is the hook to use when a component wants to render one record directly. Example: ```tsx function DeviceDetails({ device }: { device: Device | null }) { const snapshot = useEntangledObject(device); if (!snapshot) { return ; } return (

{snapshot.name}

Type
{snapshot.type}
CPUs
{snapshot.cpus}
Memory
{snapshot.memory} MB
); } ``` Caveats: - this is a snapshot of the root object only - relationship traversal is outside the snapshot boundary - callers should not mutate the returned snapshot - snapshot creation is synchronous when possible; the first render returns either a snapshot or `undefined` A synchronous snapshot is preferred when possible, because React components are easier to use when `useEntangledObject()` behaves like a normal store selector rather than a loader. ## `useEntangledValue` `useEntangledValue` is the most flexible hook in the package. It is the escape hatch for deriving exactly the value a component cares about without forcing the whole component to subscribe to every property of every object. ```ts export interface UseEntangledValueOptions { isEqual?: (left: Result, right: Result) => boolean; deps?: readonly unknown[]; } export function useEntangledValue( object: T | null | undefined, selector: (object: T) => Result, options?: UseEntangledValueOptions, ): Result | undefined; export function useEntangledValue( target: EntangledClass, selector: (objects: EntangledStorageMap) => Result, options?: UseEntangledValueOptions, ): Result; ``` Single-object example: ```tsx const imageName = useEntangledValue(device, live => live.vmImage?.name ?? 'No image', {deps: [device.id]}); ``` Type-level example: ```tsx const pendingDeviceCount = useEntangledValue( Device, devices => Array.from(devices.values()).filter(device => device.pending).length, , {deps: []}); ``` Key lookup example: ```tsx const deviceName = useEntangledValue( Device, devices => devices.get(deviceId)?.name ?? 'Unknown device', {deps: [deviceId]}); ``` Caveats: - selectors must be pure and cheap - selectors run against live objects in the storage map, not snapshots, unless the implementation explicitly snapshots first - if a selector returns a new object every time, `isEqual` becomes important to avoid unnecessary re-renders - object selectors that traverse relationships are intentionally opting into live relationship reads - for the class overload (typing `EntangledClass`), the selector is re-run when any object in the storage map changes This hook is especially useful for relationship access, aggregate counters, derived labels, and key-based row rendering where passing full objects as props would cause too many renders. ### API notes **Dependencies (`deps`)** For the class overload (selecting from `EntangledClass`), the default dependency array is `[selector]`. If the selector function is re-created on each render, provide explicit `deps` to control when re-computation occurs. For the object overload (selecting from a specific `T`), the default dependency array is `[selector, objectKey]`. If the selector changes reference but the object does not, provide `deps` to prevent unnecessary re-subscriptions. **Return value stability** The return value's identity only changes when: 1. The underlying value changes (per `isEqual`, default `Object.is`) 2. The object is deleted (returns `undefined`) If the selector returns a new object reference on every call (like creating a new array or object), use `isEqual` to provide custom equality logic. ## `useEntanglementRegistry` ```ts export function useEntanglementRegistry(): SyncRegistry; ``` Returns the `SyncRegistry` from React context. This hook stays simple. It is mostly a low-level hook for advanced integrations, testing helpers, and code that needs direct access to registration or schema-level information. Example: ```tsx function DebugRegistryInfo() { const registry = useEntanglementRegistry(); return {Array.from(registry.registry.keys()).join(', ')}; } ``` Caveat: application components rarely need this directly. Most UI uses higher-level hooks instead. ## `useEntanglementManager` ```ts export function useEntanglementManager(): SyncManager; ``` Returns the active `SyncManager` from context. Use this when the UI needs to send mutations back through Entanglement, for example create, update, or delete flows built around `syncCreate()`, `syncUpdate()`, or `syncDelete()` on a `PersistentSynchronizable`. Example: ```tsx function SaveDeviceButton({ device }: { device: Device }) { const manager = useEntanglementManager(); return ( ); } ``` Caveat: this hook exposes a live imperative API. The docs should encourage users to keep Entanglement writes in event handlers, actions, or controller-style hooks rather than inside render logic. ## `useEntangledEdit` ```ts export function useEntangledEdit( object: T | EntangledSnapshot | null | undefined, edit: (object: T) => void, deps?: readonly unknown[], ): () => Promise; ``` Returns a callback that applies an edit and then calls `syncUpdate()` as part of the hook-managed action. Example: ```tsx const renameDevice = useEntangledEdit(device, draft => { draft.name = 'foo bar'; }); ``` Semantics: - the input object may be either a live object or a snapshot - the object passed to `edit` is intentionally unspecified for now - it may be the live object - it may be a clone - after `edit` runs, the hook will `syncUpdate()` the edited object That leaves room to choose between live-object mutation and clone-then-commit based on performance and implementation experience without changing the API shape. Caveat: callers should treat the `edit` callback as a transaction boundary and should not retain the object reference passed into it. ### API notes **Dependencies (`deps`)** The default dependency array is `[manager, object, edit]`. If the edit function changes reference on each render (e.g., defined inline), provide explicit `deps` to control when a new edit callback is created. ## Context components Use one top-level provider for normal use, with optional lower-level providers only when there is a real need. ```ts export interface EntanglementProviderProps { manager: SyncManager; registry: SyncRegistry; children: ReactNode; } export function EntanglementProvider( props: EntanglementProviderProps, ): JSX.Element; ``` Example: ```tsx const registry = createRegistryFromSchema(); const manager = new SyncManager({ url: endpoint }); registry.associateManager(manager); root.render( , ); ``` Caveats: - the provider should receive already-configured objects; it should not quietly create or register them during render - `registry.associateManager(manager)` should happen before the app expects live updates - if connection lifecycle matters, a future hook such as `useEntanglementConnectionState()` may be a better place to expose status than overloading the provider # Usage guidance For the likely future generated schema based on models like `Device`, `VmImage`, and similar, usage should look like the flolowing: 1. A page component gets collection membership from `useEntangledList(Device)`. 2. Each row component receives a device key rather than a live `Device` object. 3. The row uses `useEntangledValue(Device, selector)` to look up only the fields it needs. 4. The row uses `useEntangledObject(device)` only when it really wants a full object snapshot. 5. Actions use `useEntangledEdit()` or `useEntanglementManager()` to send changes. That pattern keeps subscriptions narrow, matches React mental models, and avoids pretending Entanglement's live objects are immutable. Example: ```tsx function DevicesPage() { const devices = useEntangledList(Device, { orderBy: (left, right) => left.name.localeCompare(right.name), deps: [], }); return ( {devices.map(device => ( ))}
); } function DeviceRow({ deviceId }: { deviceId: string }) { const deviceName = useEntangledValue( Device, devices => devices.get(deviceId)?.name ?? 'Unknown device', [deviceId]); const deviceType = useEntangledValue( Device, devices => devices.get(deviceId)?.type ?? 'unknown', [deviceId]); const imageName = useEntangledValue( Device, devices => devices.get(deviceId)?.vmImage?.name ?? 'No image', [deviceId]); if (deviceName === 'Unknown device') return null; return ( {deviceName} {deviceType} {imageName} ); } ``` ## Interfaces ### EntanglementProviderProps Defined in: core.tsx:92 #### Properties ##### children > **children**: `ReactNode` Defined in: core.tsx:100 React subtree that should receive the Entanglement context. ##### manager > **manager**: `SyncManager` Defined in: core.tsx:94 Active manager used for write operations and connection-backed actions. ##### registry > **registry**: `SyncRegistry` Defined in: core.tsx:97 Registry containing the synchronizable classes available to the tree. *** ### UseEntangledListOptions Defined in: core.tsx:45 Configuration for [useEntangledList](#useentangledlist). #### Type Parameters ##### T `T` *extends* `PersistentSynchronizable` #### Properties ##### deps? > `optional` **deps?**: readonly `unknown`[] Defined in: core.tsx:72 Dependencies controlling when the hook rebuilds its filter. When omitted, the hook tracks `filter` and `orderBy` by reference. If either callback is recreated every render, pass an explicit dependency list. ##### filter? > `optional` **filter?**: (`object`) => `boolean` Defined in: core.tsx:50 Predicate evaluated against live objects to decide whether they are present in the returned membership snapshot. ###### Parameters ###### object `T` ###### Returns `boolean` ##### includeTransitions? > `optional` **includeTransitions?**: `boolean` Defined in: core.tsx:64 Whether transition events should contribute members to the list. ##### orderBy? > `optional` **orderBy?**: (`left`, `right`) => `number` Defined in: core.tsx:59 Ordering function applied when the hook rebuilds list membership. Prefer ordering on stable keys. If ordering depends on mutable object fields, sort the returned array in render code or a memoized selector instead of expecting the hook to re-sort for every field update. ###### Parameters ###### left `T` ###### right `T` ###### Returns `number` *** ### UseEntangledValueOptions Defined in: core.tsx:76 Configuration for [useEntangledValue](#useentangledvalue). #### Type Parameters ##### Result `Result` #### Properties ##### deps? > `optional` **deps?**: readonly `unknown`[] Defined in: core.tsx:89 Array of dependencies for the effect. When provided, the effect will only re-subscribe when one of these dependencies changes. When not provided, defaults to [selector, objectKey] for the object case or [selector] for the class case. ##### isEqual? > `optional` **isEqual?**: (`left`, `right`) => `boolean` Defined in: core.tsx:81 Custom equality function to determine if the derived value has changed. Defaults to Object.is. ###### Parameters ###### left `Result` ###### right `Result` ###### Returns `boolean` ## Type Aliases ### EntangledSnapshot > **EntangledSnapshot**\<`T`\> = `Readonly`\<`{ [K in keyof T]: T[K] }`\> Defined in: core.tsx:40 Shallow React-facing snapshot of a live Entanglement object. The snapshot preserves the object's fields at a moment in time, but it does not recursively snapshot related objects. Relationship getters remain a boundary back into Entanglement's live object graph. #### Type Parameters ##### T `T` ## Functions ### EntanglementProvider() > **EntanglementProvider**(`__namedParameters`): `Element` Defined in: core.tsx:163 Provides the shared Entanglement manager and registry for descendant hooks. Normal application code should use a single top-level provider with already configured `manager` and `registry` instances. #### Parameters ##### \_\_namedParameters [`EntanglementProviderProps`](#entanglementproviderprops) #### Returns `Element` *** ### useEntangledEdit() > **useEntangledEdit**\<`T`\>(`object`, `edit`, `deps?`): () => `Promise`\<`unknown`\> Defined in: core.tsx:492 Returns a callback that applies an edit and then calls `syncUpdate()` with the current manager. Use the returned callback in event handlers or action-style hooks rather than during render. #### Type Parameters ##### T `T` *extends* `PersistentSynchronizable` #### Parameters ##### object `undefined` \| `null` \| `T` \| `Readonly`\<\{ \[K in string \| number \| symbol\]: T\[K\] \}\> ##### edit (`object`) => `void` ##### deps? readonly `unknown`[] #### Returns () => `Promise`\<`unknown`\> *** ### useEntangledList() > **useEntangledList**\<`T`\>(`target`, `options?`): readonly `T`[] Defined in: core.tsx:209 Returns a React snapshot of collection membership for a synchronizable type. The returned array is replaced when membership changes, but its members are still live `PersistentSynchronizable` objects. This hook is therefore best used to discover which objects exist, while child components subscribe to the specific object fields they render. #### Type Parameters ##### T `T` *extends* `PersistentSynchronizable` #### Parameters ##### target `EntangledClass`\<`T`\> ##### options? [`UseEntangledListOptions`](#useentangledlistoptions)\<`T`\> #### Returns readonly `T`[] #### Remarks `useEntangledList` is a list-membership hook, not an object-snapshot hook. If a component renders mutable fields directly from the returned objects, it should usually also call [useEntangledObject](#useentangledobject) or [useEntangledValue](#useentangledvalue). #### Example ```tsx const devices = useEntangledList(Device, { filter: device => !device.pending, orderBy: (left, right) => left.name.localeCompare(right.name), deps: [], }); ``` *** ### useEntangledObject() > **useEntangledObject**\<`T`\>(`object`): `undefined` \| `Readonly`\<\{ \[K in string \| number \| symbol\]: T\[K\] \}\> Defined in: core.tsx:251 Returns a shallow detached snapshot of a single Entanglement object. The snapshot is safe to render in React because it is detached from the mutable master copy stored in Entanglement. Relationship traversal remains a boundary back into the live object graph; if a component needs related data, subscribe to that data explicitly with another hook. #### Type Parameters ##### T `T` *extends* `PersistentSynchronizable` #### Parameters ##### object `undefined` \| `null` \| `T` #### Returns `undefined` \| `Readonly`\<\{ \[K in string \| number \| symbol\]: T\[K\] \}\> #### Example ```tsx const snapshot = useEntangledObject(device); ``` *** ### useEntangledValue() #### Call Signature > **useEntangledValue**\<`T`, `Result`\>(`object`, `selector`, `options?`): `undefined` \| `Result` Defined in: core.tsx:355 Returns a derived value for a single object or for a synchronizable type's storage map. This is the narrow-subscription hook in the package. It lets a component subscribe to exactly the value it needs instead of forcing a whole object or whole collection to re-render. When passed a constructor, the selector receives the entire storage map and re-runs when any object in that map changes. When passed a specific object instance, the selector receives that live object and re-runs when that object changes or disappears. Selectors run against live Entanglement objects, not detached snapshots, unless the selected value itself is a `PersistentSynchronizable`, in which case the hook clones it before publishing to React state. ##### Type Parameters ###### T `T` *extends* `PersistentSynchronizable` The PersistentSynchronizable type ###### Result `Result` The type of the derived value ##### Parameters ###### object `undefined` \| `null` \| `T` The object instance to track, or null/undefined ###### selector (`object`) => `Result` A function that computes a derived value from the object ###### options? [`UseEntangledValueOptions`](#useentangledvalueoptions)\<`Result`\> Configuration options ##### Returns `undefined` \| `Result` The derived value, or undefined if the object is null/undefined or was deleted ##### Remarks For key-based lookups and aggregate values, prefer the class overload so rows and counters can subscribe to just the data they need. ##### Examples ```ts // Track a specific object's property. const name = useEntangledValue(device, live => live.name); ``` ```ts // Derive a value from all devices. const count = useEntangledValue( Device, devices => Array.from(devices.values()).filter(device => device.pending).length, { deps: [] }, ); ``` ```ts // Key-based lookup without passing a live object through props. const deviceName = useEntangledValue( Device, devices => devices.get(deviceId)?.name ?? 'Unknown device', { deps: [deviceId] }, ); ``` #### Call Signature > **useEntangledValue**\<`T`, `Result`\>(`target`, `selector`, `options?`): `Result` Defined in: core.tsx:360 Returns a derived value for a single object or for a synchronizable type's storage map. This is the narrow-subscription hook in the package. It lets a component subscribe to exactly the value it needs instead of forcing a whole object or whole collection to re-render. When passed a constructor, the selector receives the entire storage map and re-runs when any object in that map changes. When passed a specific object instance, the selector receives that live object and re-runs when that object changes or disappears. Selectors run against live Entanglement objects, not detached snapshots, unless the selected value itself is a `PersistentSynchronizable`, in which case the hook clones it before publishing to React state. ##### Type Parameters ###### T `T` *extends* `PersistentSynchronizable` The PersistentSynchronizable type ###### Result `Result` The type of the derived value ##### Parameters ###### target `EntangledClass`\<`T`\> ###### selector (`objects`) => `Result` A function that computes a derived value from the object ###### options? [`UseEntangledValueOptions`](#useentangledvalueoptions)\<`Result`\> Configuration options ##### Returns `Result` The derived value, or undefined if the object is null/undefined or was deleted ##### Remarks For key-based lookups and aggregate values, prefer the class overload so rows and counters can subscribe to just the data they need. ##### Examples ```ts // Track a specific object's property. const name = useEntangledValue(device, live => live.name); ``` ```ts // Derive a value from all devices. const count = useEntangledValue( Device, devices => Array.from(devices.values()).filter(device => device.pending).length, { deps: [] }, ); ``` ```ts // Key-based lookup without passing a live object through props. const deviceName = useEntangledValue( Device, devices => devices.get(deviceId)?.name ?? 'Unknown device', { deps: [deviceId] }, ); ``` *** ### useEntanglementManager() > **useEntanglementManager**(): `SyncManager` Defined in: core.tsx:183 Returns the active `SyncManager` from context. Use this hook for imperative write flows, such as creating, updating, or deleting synchronizable objects from event handlers or controller-style hooks. #### Returns `SyncManager` *** ### useEntanglementRegistry() > **useEntanglementRegistry**(): `SyncRegistry` Defined in: core.tsx:173 Returns the active `SyncRegistry` from context. This is primarily a low-level escape hatch for advanced integrations, tests, and code that needs direct access to registry metadata. #### Returns `SyncRegistry`