Skip to content

ClientReady#56

Merged
adebola-io merged 3 commits into
mainfrom
client-ready
May 10, 2026
Merged

ClientReady#56
adebola-io merged 3 commits into
mainfrom
client-ready

Conversation

@adebola-io
Copy link
Copy Markdown
Collaborator

No description provided.

adebola-io added 3 commits May 8, 2026 16:16
Introduce a scope for managing XML namespaces. This allows for proper
handling of SVG and MathML elements within nested structures, including
reactive branches and teleported content.

This change also improves the `createContainer` logic by inheriting
namespaces from parent scopes and setting them explicitly when needed,
rather than relying on string manipulation or re-serialization.
Introduce ClientReady component and move ClientOnly into
client-boundaries.js.

Update package.json and documentation to reflect the changes.
@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Introduce ClientReady component and refactor client boundaries

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Introduce ClientReady component for async-aware client rendering
• Consolidate ClientOnly into new client-boundaries.js module
• Update exports and documentation to reflect new structure
• Add comprehensive tests for ClientReady behavior
Diagram
flowchart LR
  A["client-only.js<br/>ClientOnly"] -->|consolidate| B["client-boundaries.js<br/>ClientOnly + ClientReady"]
  B -->|export| C["package.json<br/>Updated exports"]
  B -->|document| D["Documentation<br/>ClientOnly & ClientReady"]
  B -->|test| E["Tests<br/>ClientReady scenarios"]
Loading

Grey Divider

File Changes

1. packages/retend-server/source/client-boundaries.js ✨ Enhancement +104/-0

New client boundaries module with ClientReady

• New file consolidating ClientOnly component from client-only.js
• Introduces ClientReady component that waits for async client content
• ClientReady combines ClientOnly with Await boundary for readiness gating
• Both components properly documented with JSDoc and usage examples

packages/retend-server/source/client-boundaries.js


2. packages/retend-server/source/client-only.js Refactoring +0/-51

Removed in favor of client-boundaries consolidation

• File deleted and consolidated into client-boundaries.jsClientOnly component moved to new module structure

packages/retend-server/source/client-only.js


3. docs/content/24-client-boundaries.mdx 📝 Documentation +20/-2

Document ClientReady and update client boundaries guide

• Updated title to include both ClientOnly and ClientReady
• Added new section documenting ClientReady component usage
• Included example showing ClientReady with async dashboard content
• Clarified use cases for when to use each component

docs/content/24-client-boundaries.mdx


View more (2)
4. packages/retend-server/package.json ⚙️ Configuration changes +2/-2

Update exports to client-boundaries module

• Updated main export path from client-only.js to client-boundaries.js
• Updated TypeScript definitions path accordingly

packages/retend-server/package.json


5. tests/hydration/client-boundaries.spec.tsx 🧪 Tests +101/-1

Add ClientReady test coverage for all scenarios

• Added import for ClientReady component
• Added comprehensive test suite for ClientReady with three scenarios
• Tests cover SSR rendering, hydration with async content, and SPA mode
• Validates fallback visibility during async resolution

tests/hydration/client-boundaries.spec.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented May 10, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Action required

1. Fallback/content overlap 🐞 Bug ≡ Correctness
Description
ClientReady can briefly render the resolved client subtree while still rendering fallback,
because ready is flipped in an onSetup effect that runs after the DOM has already been updated
to show the Await content. This can produce a flash/layout shift (content appears before fallback
is removed) and breaks the promise that the fallback remains until the subtree is ready, then
disappears.
Code

packages/retend-server/source/client-boundaries.js[R84-103]

+export function ClientReady(props) {
+  const { children, fallback } = props;
+  const ready = Cell.source(false);
+
+  const ClientReadyContent = () => {
+    onSetup(() => ready.set(true));
+    return children;
+  };
+
+  return [
+    ClientOnly({
+      children: () =>
+        Await({
+          children: ClientReadyContent,
+        }),
+    }),
+    If(ready, {
+      false: () => fallback,
+    }),
+  ];
Evidence
In ClientReady, the fallback is controlled by a separate If(ready, { false: () => fallback }),
while ready is only set inside ClientReadyContent via onSetup. In Retend, If updates the
rendered nodes first (renderer.write + flush) and only then triggers effect activation
(stateSnapshot.node.activate()), and effect activation itself is delayed by a setTimeout. This
ordering guarantees a window where the awaited subtree can be written to the DOM while ready is
still false, so the fallback is still present (potentially alongside the content).

packages/retend-server/source/client-boundaries.js[84-103]
packages/retend/source/library/if.js[152-158]
packages/retend/source/library/scope.js[139-144]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ClientReady` currently renders fallback via a separate `If(ready)` sibling, but only sets `ready` in an `onSetup` effect inside the awaited subtree. Because Retend applies DOM updates before running `onSetup` effects (and activation is delayed), the awaited content can become visible while the fallback is still rendered, causing a brief overlap/layout shift.

### Issue Context
`Await` already supports `fallback` and will keep rendering it until its initial async dependencies resolve. `ClientOnly` can also render the same `fallback` during SSR/before mount.

### Fix Focus Areas
- packages/retend-server/source/client-boundaries.js[84-103]

### Suggested fix
Refactor `ClientReady` to rely on `ClientOnly` + `Await` fallback instead of a separate `ready` cell:

```js
export function ClientReady(props) {
 const { children, fallback } = props;

 return ClientOnly({
   fallback,
   children: () =>
     Await({
       fallback: fallback ?? null,
       children,
     }),
 });
}
```

This keeps the fallback in exactly one place and prevents the “content appears before fallback is removed” window.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
retend 95d851a Commit Preview URL

Branch Preview URL
May 10 2026, 03:40 PM

@adebola-io adebola-io merged commit e9076ea into main May 10, 2026
4 checks passed
@adebola-io adebola-io deleted the client-ready branch May 10, 2026 15:41
Comment on lines +84 to +103
export function ClientReady(props) {
const { children, fallback } = props;
const ready = Cell.source(false);

const ClientReadyContent = () => {
onSetup(() => ready.set(true));
return children;
};

return [
ClientOnly({
children: () =>
Await({
children: ClientReadyContent,
}),
}),
If(ready, {
false: () => fallback,
}),
];
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.

Action required

1. Fallback/content overlap 🐞 Bug ≡ Correctness

ClientReady can briefly render the resolved client subtree while still rendering fallback,
because ready is flipped in an onSetup effect that runs after the DOM has already been updated
to show the Await content. This can produce a flash/layout shift (content appears before fallback
is removed) and breaks the promise that the fallback remains until the subtree is ready, then
disappears.
Agent Prompt
### Issue description
`ClientReady` currently renders fallback via a separate `If(ready)` sibling, but only sets `ready` in an `onSetup` effect inside the awaited subtree. Because Retend applies DOM updates before running `onSetup` effects (and activation is delayed), the awaited content can become visible while the fallback is still rendered, causing a brief overlap/layout shift.

### Issue Context
`Await` already supports `fallback` and will keep rendering it until its initial async dependencies resolve. `ClientOnly` can also render the same `fallback` during SSR/before mount.

### Fix Focus Areas
- packages/retend-server/source/client-boundaries.js[84-103]

### Suggested fix
Refactor `ClientReady` to rely on `ClientOnly` + `Await` fallback instead of a separate `ready` cell:

```js
export function ClientReady(props) {
  const { children, fallback } = props;

  return ClientOnly({
    fallback,
    children: () =>
      Await({
        fallback: fallback ?? null,
        children,
      }),
  });
}
```

This keeps the fallback in exactly one place and prevents the “content appears before fallback is removed” window.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant