feat(react): add useObject hook for LiveObjects#2259
Conversation
Adds a new `ably/liveobjects/react` entry point exporting `useObject`, which subscribes to a channel's LiveObjects state (optionally a nested node selected by navigating from the root) and re-renders on change, built on useSyncExternalStore with a cached compact() snapshot.
WalkthroughAdds a new ChangesuseObject React hook for LiveObjects
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Component
participant useObject
participant Channel
participant PathObject
Component->>useObject: render selector and options
useObject->>Channel: resolve channel.object.get()
Channel-->>useObject: return root PathObject
useObject->>PathObject: navigate selector and subscribe
PathObject-->>useObject: notify object change
useObject-->>Component: return cached value and object state
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@package.json`:
- Around line 43-46: The ./liveobjects/react export in package.json only points
to JS, so add a types condition alongside the existing require/import entries so
TypeScript can resolve declarations for ably/liveobjects/react. Update the
export block for this subpath consistently with the other package exports, and
make sure the new types target points at the matching declaration entry for the
React liveobjects module.
In `@src/platform/react-hooks/src/liveobjects/useObject.ts`:
- Around line 232-253: The ObjectStore snapshot logic in useObject still returns
the render-time cached value, so a change between render and subscription setup
can remain stale. Update the subscribe path in useObject’s useMemo store so that
after node.subscribe() completes, it immediately re-reads node.compact() and
refreshes the cached value only if it is still current, using a stable
compare/version check to avoid extra re-renders. Keep getSnapshot backed by the
updated cache so useSyncExternalStore sees the latest snapshot right after
subscribing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 827d3020-f00d-4443-a3ab-61ac64a7d1e7
📒 Files selected for processing (7)
docs/react.mdpackage.jsonsrc/platform/react-hooks/src/fakes/liveobjects.tssrc/platform/react-hooks/src/liveobjects/index.tssrc/platform/react-hooks/src/liveobjects/useObject.test.tsxsrc/platform/react-hooks/src/liveobjects/useObject.tssrc/platform/react-hooks/tsconfig.json
VeskeR
left a comment
There was a problem hiding this comment.
LGTM overall, couple of minor requests below and a question about types.
Also, one of the core benefits of the typed navigation selector we chose is, well, that types flow through. We'd want to ensure this actually works. I think it's worthwhile to add couple of simple checks to our package tests in https://github.com/ably/ably-js/blob/main/test/package/browser/template/src/ReactApp.tsx, and check that useObject resolves to correct types as expected
| </ChannelProvider> | ||
| ``` | ||
|
|
||
| Called with no arguments, `useObject` subscribes to the channel's root object. `value` is a plain-data snapshot of the object (the result of [`compact()`](https://ably.com/docs/liveobjects/concepts/objects#compact)), and `object` is the live `PathObject` for the subscribed node, which you can use to navigate and to write: |
There was a problem hiding this comment.
I know this is annoying, but we don't use "root" terminology anymore, see https://ably.atlassian.net/wiki/spaces/LOB/pages/4235722804/LODR-042+LiveObjects+Realtime+Client+API+Improvements#Entrypoint:
We also remove any reference to the “root” object altogether; this concept was previously used to distinguish between the root entrypoint object and nested objects. In a single-object-per-channel mental model, this concept is no longer needed.
Essentially, in public docs we just say "channel's object". Should update throughout the public docs/jsdocs/types.
It's okay to use "root" internally though, if it would make things clearer in the implementation.
|
|
||
| For the no-selector form, provide the root type as a type parameter instead: `useObject<GameRoot>()`. | ||
|
|
||
| The root object resolves asynchronously (after the channel attaches and the initial sync completes), so the hook's readiness is derived from the result: `object === undefined && error === null` means loading, `error !== null` means the object could not be resolved (for example the `LiveObjects` plugin is missing, or the channel lacks the object modes — `error` carries ably-js's own `ErrorInfo`), and a defined `object` means ready, with `value` carrying the data once the node exists. |
There was a problem hiding this comment.
This is the first mention of error and it's not clear that this is one of the return values from useObject and what it contains. Probably one more paragraph with code sample above to showcase it?
|
|
||
| const AliceScore = () => { | ||
| // value: number | undefined, object: PathObject<LiveCounter> | undefined | ||
| const { value, object } = useObject((root: PathObject<GameRoot>) => root.get('scores').get('alice')); |
There was a problem hiding this comment.
nit: can we pass a type parameter to the hook in all code samples for consistency with "no-selector" form? Could be cleaner
There was a problem hiding this comment.
I think this statement also should be updated to reflect that useObject requires 18+
| * (which is what makes the selector's navigation chain determine the result | ||
| * type) without forcing them into a single `PathObject<V>` shape. | ||
| */ | ||
| export interface ObjectNode { |
There was a problem hiding this comment.
It's not immediately clear to me why this is required and we can't use the types from liveobjects.d.ts where we have <T extends Value = Value> -> PathObject<T>. Could you please clarify it, in a response here or in an inline code comment?
| ```tsx | ||
| import { LiveCounter, LiveMap, PathObject } from 'ably/liveobjects'; | ||
|
|
||
| type GameRoot = LiveMap<{ |
There was a problem hiding this comment.
Also noticed that useObject hook expects a LiveMap type for the channel object, but non-hooks version RealtimeObject.get is typed get<T extends Record<string, Value>>(): Promise<PathObject<LiveMap<T>>>; https://github.com/ably/ably-js/blob/main/liveobjects.d.ts#L589, so it expects an inner object shape.
Not sure if we can make useObject work the same due to ObjectNode
| * `error` carries ably-js's reason. Readiness is derived from the result (see | ||
| * {@link UseObjectResult.object}), not a status enum. | ||
| */ | ||
| export function useObject<Root extends Value = LiveMap<Record<string, Value>>>( |
There was a problem hiding this comment.
I think the unintended effect of Root extends Value is that you can pass LiveCounter:
useObject<LiveCounter>()which is nonsensical
| * Channel resolution, readiness, and plugin/modes requirements are as for the | ||
| * root overload. | ||
| */ | ||
| export function useObject<Root extends ObjectNode, N extends ObjectNode>( |
There was a problem hiding this comment.
wdyt about these types for useObject's selector? the root is always a map, so it doesn't need to be Root extends ObjectNode - we can type it with the real thing (same style as channel.object.get<T>()):
export function useObject<T extends Record<string, Value> = Record<string, Value>, N extends ObjectNode = ObjectNode>(
selector: (root: PathObject<LiveMap<T>>) => N,
options?: UseObjectOptions,
): UseObjectResult<N>;As a result you can't do useObject<LiveCounter>()
- refresh the cached snapshot after subscribing so a change landing between render and subscription setup is not missed - add a types condition to the ably/liveobjects/react export - type the no-selector overload with the inner object shape, matching RealtimeObject.get<T>() and rejecting non-map shapes - say "the channel's object" rather than "root" in public docs and JSDoc - document the error return value and the React 18 requirement - add type-flow checks for useObject to the NPM package test app
…object The typed-selector overload takes (object: PathObject<LiveMap<T>>) => N, inferring T from the selector's annotation — the same inner-shape type parameter RealtimeObject.get<T>() takes — and rejecting selectors that annotate the channel's object as a non-map node.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/react.md`:
- Line 33: Update the React compatibility statement in the hooks documentation
to say “React 16.8.0 or later” instead of “above 16.8.0,” while preserving the
exception that useObject requires React 18 or later.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1cb61216-9b69-411d-874d-9585c1b45541
📒 Files selected for processing (6)
docs/react.mdpackage.jsonsrc/platform/react-hooks/src/fakes/liveobjects.tssrc/platform/react-hooks/src/liveobjects/useObject.test.tsxsrc/platform/react-hooks/src/liveobjects/useObject.tstest/package/browser/template/src/ReactApp.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- package.json
- src/platform/react-hooks/src/liveobjects/useObject.test.tsx
- src/platform/react-hooks/src/fakes/liveobjects.ts
- src/platform/react-hooks/src/liveobjects/useObject.ts
| ### Compatible React Versions | ||
|
|
||
| The hooks are compatible with all versions of React above 16.8.0 | ||
| The hooks are compatible with all versions of React above 16.8.0, with the exception of `useObject`, which requires React 18 or later. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use inclusive React version wording.
“Above 16.8.0” technically excludes React 16.8.0. Use “React 16.8.0 or later” to state the supported range precisely.
Proposed wording
-The hooks are compatible with all versions of React above 16.8.0, with the exception of `useObject`, which requires React 18 or later.
+The hooks are compatible with React 16.8.0 or later, with the exception of `useObject`, which requires React 18 or later.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| The hooks are compatible with all versions of React above 16.8.0, with the exception of `useObject`, which requires React 18 or later. | |
| The hooks are compatible with React 16.8.0 or later, with the exception of `useObject`, which requires React 18 or later. |
🧰 Tools
🪛 LanguageTool
[style] ~33-~33: Consider using “except” or “except for”
Context: ...ith all versions of React above 16.8.0, with the exception of useObject, which requires React 18 or...
(WITH_THE_EXCEPTION_OF)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/react.md` at line 33, Update the React compatibility statement in the
hooks documentation to say “React 16.8.0 or later” instead of “above 16.8.0,”
while preserving the exception that useObject requires React 18 or later.
Source: Linters/SAST tools
AIT-1133
Implements PSDR20
Adds a new
ably/liveobjects/reactentry point exportinguseObject, which subscribes to a channel's LiveObjects state (optionally a nested node selected by navigating from the root) and re-renders on change, built on useSyncExternalStore with a cached compact() snapshot.Summary by CodeRabbit
importandrequire.