Skip to content

feat(react): add useObject hook for LiveObjects#2259

Open
owenpearson wants to merge 3 commits into
mainfrom
useobject-hook
Open

feat(react): add useObject hook for LiveObjects#2259
owenpearson wants to merge 3 commits into
mainfrom
useobject-hook

Conversation

@owenpearson

@owenpearson owenpearson commented Jul 9, 2026

Copy link
Copy Markdown
Member

AIT-1133

Implements PSDR20

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.

Summary by CodeRabbit

  • New Features
    • Added a new React hook for LiveObjects with root/nested access, selector-based subscriptions, typed loading states, and layered error reporting.
    • Exposed React LiveObjects via a new package import path for both import and require.
    • Updated React documentation with setup requirements, version guidance, caching/value semantics, and usage patterns.
  • Tests
    • Added hook tests covering resolution lifecycle, selector updates, identity stability, intermediate timing changes, unmount cleanup, skip behavior, error surfacing, and channel configuration.
  • Chores
    • Added TypeScript path aliasing and re-exports to support the new React LiveObjects entry point.

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.
@owenpearson
owenpearson requested a review from VeskeR July 9, 2026 11:01
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new useObject React hook for subscribing to Ably LiveObjects PathObject nodes with optional selectors, backed by useSyncExternalStore. Includes supporting fakes, tests, package exports, type checks, configuration, and documentation.

Changes

useObject React hook for LiveObjects

Layer / File(s) Summary
Public types and resolution contracts
src/platform/react-hooks/src/liveobjects/useObject.ts
Defines the hook’s exported node, selector, result, options, overload, SSR, and store types.
Root resolution and reactive subscription
src/platform/react-hooks/src/liveobjects/useObject.ts
Resolves the root object, applies selectors, stabilizes selected nodes, caches snapshots, subscribes through useSyncExternalStore, and returns connection/channel errors.
Fake LiveObjects implementation
src/platform/react-hooks/src/fakes/liveobjects.ts
Adds in-memory root and path objects with deferred or failing get(), nested writes, path-overlap notifications, subscriptions, and compact snapshots.
Behavioral test suite
src/platform/react-hooks/src/liveobjects/useObject.test.tsx
Tests resolution, selectors, rerender isolation, identity stability, timing, errors, skipping, cleanup, selector relocation, and channel options.
Package exposure and type checks
package.json, src/platform/react-hooks/tsconfig.json, src/platform/react-hooks/src/liveobjects/index.ts, test/package/browser/template/src/ReactApp.tsx
Adds the React LiveObjects export and TypeScript alias, re-exports the hook, and verifies root and selector-based typing.
React hook documentation
docs/react.md
Documents setup, React 18 requirements, selector usage, typing, readiness, snapshots, and error fields.

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
Loading

Suggested reviewers: ttypic

Poem

A rabbit hops through paths so neat,
Live snapshots dance on tiny feet.
Select a node, let changes flow,
Errors and ready states now show.
“React 18!” the bunnies cheer—
A clever hook is burrowed here.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a React useObject hook for LiveObjects.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch useobject-hook

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions
github-actions Bot temporarily deployed to staging/pull/2259/features July 9, 2026 11:06 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2259/bundle-report July 9, 2026 11:06 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2259/typedoc July 9, 2026 11:06 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a398ce3 and fe72bb0.

📒 Files selected for processing (7)
  • docs/react.md
  • package.json
  • src/platform/react-hooks/src/fakes/liveobjects.ts
  • src/platform/react-hooks/src/liveobjects/index.ts
  • src/platform/react-hooks/src/liveobjects/useObject.test.tsx
  • src/platform/react-hooks/src/liveobjects/useObject.ts
  • src/platform/react-hooks/tsconfig.json

Comment thread package.json
Comment thread src/platform/react-hooks/src/liveobjects/useObject.ts

@VeskeR VeskeR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs/react.md Outdated
</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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/react.md Outdated

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread docs/react.md Outdated

const AliceScore = () => {
// value: number | undefined, object: PathObject<LiveCounter> | undefined
const { value, object } = useObject((root: PathObject<GameRoot>) => root.get('scores').get('alice'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we pass a type parameter to the hook in all code samples for consistency with "no-selector" form? Could be cleaner

Comment thread src/platform/react-hooks/src/liveobjects/useObject.ts
Comment thread docs/react.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread docs/react.md Outdated
```tsx
import { LiveCounter, LiveMap, PathObject } from 'ably/liveobjects';

type GameRoot = LiveMap<{

@VeskeR VeskeR Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>>>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fe72bb0 and 909182e.

📒 Files selected for processing (6)
  • docs/react.md
  • package.json
  • src/platform/react-hooks/src/fakes/liveobjects.ts
  • src/platform/react-hooks/src/liveobjects/useObject.test.tsx
  • src/platform/react-hooks/src/liveobjects/useObject.ts
  • test/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

Comment thread docs/react.md
### 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants