Skip to content

Conversation

@Yradex
Copy link
Collaborator

@Yradex Yradex commented Jul 18, 2025

Summary

Support using a host element as direct child of Suspense.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

Summary by CodeRabbit

  • New Features
    • Improved Suspense component now allows direct use of host elements (such as DOM elements) as children.
  • Bug Fixes
    • Enhanced lifecycle management to ensure proper cleanup and restoration of elements during Suspense operations.
    • Introduced explicit tracking and reconstruction of removed nodes to improve rendering consistency.
  • Tests
    • Added comprehensive tests covering Suspense behaviors, including fallback rendering, cleanup, nested and parallel Suspense, error boundaries, and resolved promises.
  • Style
    • Updated test snapshots to reflect new wrapper structure in rendered output.

@changeset-bot
Copy link

changeset-bot bot commented Jul 18, 2025

🦋 Changeset detected

Latest commit: 41d6a93

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@lynx-js/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 18, 2025

📝 Walkthrough

Walkthrough

This change introduces a new internal implementation of the Suspense component in the Lynx React runtime, enabling a host element to be a direct child of Suspense. It updates background snapshot logic to support tree reconstruction, adds comprehensive Suspense tests covering lifecycle and edge cases, and adjusts test snapshots to reflect new wrapper elements.

Changes

Files/Paths Change Summary
.changeset/fast-coats-swim.md Added changelog entry for new Suspense feature supporting host element as direct child.
packages/react/runtime/src/lynx/suspense.ts Introduced new internal Suspense component handling wrapper logic and snapshot management.
packages/react/runtime/src/backgroundSnapshot.ts Added tree removal tracking and reconstruction logic for background snapshots; refactored hydrate helper.
packages/react/runtime/src/index.ts, packages/react/runtime/src/internal.ts Switched Suspense import from external (preact/compat) to new internal implementation.
packages/react/runtime/test/lynx/suspense.test.jsx Added comprehensive unit tests for Suspense lifecycle, edge cases, and host element support.
packages/react/testing-library/src/tests/lazy-bundle/index.test.jsx Updated test snapshots to include new wrapper elements reflecting Suspense changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • hzy
  • colinaaa

Poem

In the warren of code, a Suspenseful delight,
Host elements now nestle in wrappers so tight.
Snapshots rebuilt, tests hop in with glee,
New logic abounds in the Lynx family tree.
With every patch, this rabbit’s heart skips—
Awaiting review with twitching whisker tips! 🐇

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 231a08f and 0e9abdd.

📒 Files selected for processing (1)
  • packages/react/runtime/src/internal.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react/runtime/src/internal.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Yradex Yradex changed the title support using a host element as direct child of Suspense feat(react/runtime): support using a host element as direct child of Suspense Jul 18, 2025
@codecov
Copy link

codecov bot commented Jul 18, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link

codspeed-hq bot commented Jul 18, 2025

CodSpeed Performance Report

Merging #1308 will not alter performance

Comparing Yradex:suspense/element-in-suspense (41d6a93) with main (29434ae)

Summary

✅ 10 untouched benchmarks

@relativeci
Copy link

relativeci bot commented Jul 18, 2025

React Example

#3717 Bundle Size — 235.26KiB (+0.05%).

41d6a93(current) vs 29434ae main#3712(baseline)

Bundle metrics  Change 4 changes Regression 1 regression
                 Current
#3717
     Baseline
#3712
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 38.01% 0%
No change  Chunks 0 0
No change  Assets 4 4
Change  Modules 159(+1.92%) 156
Regression  Duplicate Modules 64(+1.59%) 63
Change  Duplicate Code 45.81%(-0.28%) 45.94%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#3717
     Baseline
#3712
No change  IMG 145.76KiB 145.76KiB
Regression  Other 89.5KiB (+0.14%) 89.38KiB

Bundle analysis reportBranch Yradex:suspense/element-in-suspe...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link

relativeci bot commented Jul 18, 2025

Web Explorer

#3707 Bundle Size — 342.52KiB (0%).

41d6a93(current) vs 29434ae main#3702(baseline)

Bundle metrics  no changes
                 Current
#3707
     Baseline
#3702
No change  Initial JS 142.05KiB 142.05KiB
No change  Initial CSS 31.84KiB 31.84KiB
Change  Cache Invalidation 0% 65.76%
No change  Chunks 7 7
No change  Assets 7 7
No change  Modules 211 211
No change  Duplicate Modules 17 17
No change  Duplicate Code 3.96% 3.96%
No change  Packages 4 4
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#3707
     Baseline
#3702
No change  JS 227.57KiB 227.57KiB
No change  Other 83.11KiB 83.11KiB
No change  CSS 31.84KiB 31.84KiB

Bundle analysis reportBranch Yradex:suspense/element-in-suspe...Project dashboard


Generated by RelativeCIDocumentationReport issue

@Yradex Yradex force-pushed the suspense/element-in-suspense branch 2 times, most recently from 31134dc to 674fa03 Compare July 21, 2025 07:32
@Yradex Yradex marked this pull request as ready for review July 21, 2025 07:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (1)
packages/react/runtime/src/snapshot.ts (1)

579-582: Potential issue with marking list items as deleted

Setting child.__id = 0 for list items could cause issues if the instance is accessed before actual destruction. Consider using a different approach to mark deletion state.

Consider adding a dedicated flag instead:

       this.__removeChild(child);
-      // mark this child as deleted
-      child.__id = 0;
+      // mark this child as deleted
+      child.__markedForDeletion = true;
       return;
🧹 Nitpick comments (6)
packages/react/runtime/src/list.ts (1)

147-151: Guard fix looks good; consider tightening the flow

The new if (__id) … else … guard prevents unRenderElements() from running on instances without an element – 👍.
You could fold the subsequent if (!__id) … else if … into the first branch to avoid checking __id twice, but functionally it’s fine.

packages/react/runtime/__test__/preact.test.jsx (1)

17-18: Consider moving cleanup to a shared helper

Multiple test files now replicate destroyRemovedSnapshotInstances() calls.
Extract a small helper or add it to a global afterEach hook to avoid duplication and guarantee coverage across new tests.

packages/react/runtime/__test__/snapshotPatch.test.jsx (1)

710-730: Consider adding a success case test for PreventDestroy.

While the error case is well-tested, consider adding a test that verifies the successful execution of PreventDestroy on a valid snapshot instance.

describe('PreventDestroy', () => {
  beforeEach(() => {
    initGlobalSnapshotPatch();
  });

  it('error', async function() {
    initGlobalSnapshotPatch();
    const patch = takeGlobalSnapshotPatch();
    patch.push(SnapshotOperation.PreventDestroy, 100);
    snapshotPatchApply(patch);
    expect(_ReportError).toHaveBeenCalledTimes(1);
    expect(_ReportError.mock.calls[0]).toMatchInlineSnapshot(`
      [
        [Error: snapshotPatchApply failed: ctx not found, snapshot type: 'null'],
        {
          "errorCode": 1101,
        },
      ]
    `);
  });
+
+  it('success', async function() {
+    const bsi1 = new BackgroundSnapshotInstance(snapshot1);
+    let patch = takeGlobalSnapshotPatch();
+    snapshotPatchApply(patch);
+    
+    const si1 = snapshotInstanceManager.values.get(bsi1.__id);
+    const preventDestroySpy = vi.spyOn(si1, 'preventDestroy');
+    
+    patch = takeGlobalSnapshotPatch();
+    patch.push(SnapshotOperation.PreventDestroy, bsi1.__id);
+    snapshotPatchApply(patch);
+    
+    expect(preventDestroySpy).toHaveBeenCalledTimes(1);
+    expect(_ReportError).not.toHaveBeenCalled();
+  });
});
packages/react/runtime/src/lynx/suspense.ts (1)

22-23: Consider defining a proper type for the wrapper element.

Instead of suppressing TypeScript errors with @ts-expect-error, consider properly typing the wrapper element or using a type assertion.

+  type WrapperProps = {
+    ref?: (instance: BackgroundSnapshotInstance) => void;
+    children?: VNode | VNode[];
+  };
+
   // @ts-expect-error wrapper is a valid element type
-  const newChildren = __createElement('wrapper', {
+  const newChildren = __createElement('wrapper' as any, {
     ref: (bsi: BackgroundSnapshotInstance) => {
       if (bsi) {
         childrenRef.current = bsi;
       }
     },
-  }, children);
+  } as WrapperProps, children);

Also applies to: 31-32

packages/react/runtime/src/snapshot.ts (1)

644-650: Consider optimization for frequent operations

The current implementation has O(n) complexity for both search and removal. If this method is called frequently or the array grows large, consider using a Set for better performance.

If performance becomes a concern:

-export const snapshotInstancesToDestroy: number[] = [];
+export const snapshotInstancesToDestroy = new Set<number>();

 export function destroyRemovedSnapshotInstances(): void {
-  for (const id of snapshotInstancesToDestroy) {
+  for (const id of snapshotInstancesToDestroy) {
     snapshotInstanceManager.values.get(id)?.tearDown();
   }
-  snapshotInstancesToDestroy.length = 0;
+  snapshotInstancesToDestroy.clear();
 }

 // In removeChild:
-    snapshotInstancesToDestroy.push(child.__id);
+    snapshotInstancesToDestroy.add(child.__id);

 // In preventDestroy:
   preventDestroy(): void {
-    const index = snapshotInstancesToDestroy.indexOf(this.__id);
-    if (index !== -1) {
-      snapshotInstancesToDestroy.splice(index, 1);
-    }
+    snapshotInstancesToDestroy.delete(this.__id);
   }
packages/react/runtime/__test__/lynx/suspense.test.jsx (1)

450-459: Address TODO: Clean up wrapper elements from snapshotInstanceManager

The comment indicates that wrapper elements created by createElement in suspense (-2 and -3) remain in snapshotInstanceManager. This could lead to memory leaks over time.

Would you like me to help create a cleanup mechanism for these wrapper elements or open an issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd5ea2 and 674fa03.

📒 Files selected for processing (17)
  • .changeset/fast-coats-swim.md (1 hunks)
  • packages/react/runtime/__test__/basic.test.jsx (2 hunks)
  • packages/react/runtime/__test__/debug/formatPatch.test.ts (2 hunks)
  • packages/react/runtime/__test__/lynx/suspense.test.jsx (1 hunks)
  • packages/react/runtime/__test__/preact.test.jsx (2 hunks)
  • packages/react/runtime/__test__/renderToOpcodes.test.jsx (2 hunks)
  • packages/react/runtime/__test__/snapshotPatch.test.jsx (1 hunks)
  • packages/react/runtime/src/backgroundSnapshot.ts (2 hunks)
  • packages/react/runtime/src/index.ts (1 hunks)
  • packages/react/runtime/src/internal.ts (1 hunks)
  • packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts (2 hunks)
  • packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts (1 hunks)
  • packages/react/runtime/src/lifecycle/patch/updateMainThread.ts (2 hunks)
  • packages/react/runtime/src/list.ts (1 hunks)
  • packages/react/runtime/src/lynx/suspense.ts (1 hunks)
  • packages/react/runtime/src/snapshot.ts (4 hunks)
  • packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx (1 hunks)
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: colinaaa
PR: lynx-family/lynx-stack#1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.
Learnt from: PupilTong
PR: lynx-family/lynx-stack#1029
File: packages/web-platform/web-core-server/src/createLynxView.ts:0-0
Timestamp: 2025-07-16T06:26:22.177Z
Learning: In the lynx-stack SSR implementation, each createLynxView instance is used to render once and then discarded. There's no reuse of the same instance for multiple renders, so event arrays and other state don't need to be cleared between renders.
packages/react/runtime/src/index.ts (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/__test__/basic.test.jsx (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/__test__/preact.test.jsx (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/src/internal.ts (4)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

Learnt from: PupilTong
PR: #1029
File: packages/web-platform/web-core-server/src/createLynxView.ts:0-0
Timestamp: 2025-07-16T06:26:22.177Z
Learning: In the lynx-stack SSR implementation, each createLynxView instance is used to render once and then discarded. There's no reuse of the same instance for multiple renders, so event arrays and other state don't need to be cleared between renders.

Learnt from: PupilTong
PR: #1029
File: packages/web-platform/web-core/src/uiThread/createRenderAllOnUI.ts:95-99
Timestamp: 2025-07-16T06:28:26.421Z
Learning: In the lynx-stack codebase, CSS selectors in SSR hydration are generated by their own packages, ensuring a predictable format that makes simple string manipulation safe and preferable over regex for performance reasons.

Learnt from: PupilTong
PR: #1292
File: packages/web-platform/web-core-server/src/createLynxView.ts:144-151
Timestamp: 2025-07-15T10:00:56.154Z
Learning: In the lynx-stack codebase, PupilTong prefers the "let it crash" approach over defensive null safety checks when the condition should never occur in normal operation. This applies to cases like the element.getAttribute(lynxUniqueIdAttribute)! call in SSR event capture where the attribute is expected to always be present.

packages/react/runtime/__test__/renderToOpcodes.test.jsx (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/src/list.ts (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/src/lynx/suspense.ts (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

packages/react/runtime/__test__/lynx/suspense.test.jsx (1)

Learnt from: colinaaa
PR: #1238
File: packages/react/runtime/src/debug/component-stack.ts:70-90
Timestamp: 2025-07-18T04:27:18.263Z
Learning: The component-stack.ts file in packages/react/runtime/src/debug/component-stack.ts is a direct fork from https://github.com/preactjs/preact/blob/main/debug/src/component-stack.js. The team prefers to keep it aligned with the upstream Preact version and may contribute improvements back to Preact in the future.

🧬 Code Graph Analysis (6)
packages/react/runtime/src/lifecycle/patch/updateMainThread.ts (1)
packages/react/runtime/src/snapshot.ts (1)
  • destroyRemovedSnapshotInstances (185-190)
packages/react/runtime/__test__/basic.test.jsx (1)
packages/react/runtime/src/snapshot.ts (2)
  • snapshotInstanceManager (123-134)
  • destroyRemovedSnapshotInstances (185-190)
packages/react/runtime/__test__/preact.test.jsx (1)
packages/react/runtime/src/snapshot.ts (1)
  • destroyRemovedSnapshotInstances (185-190)
packages/react/runtime/__test__/debug/formatPatch.test.ts (1)
packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts (1)
  • SnapshotOperation (11-21)
packages/react/runtime/__test__/renderToOpcodes.test.jsx (1)
packages/react/runtime/src/snapshot.ts (1)
  • destroyRemovedSnapshotInstances (185-190)
packages/react/runtime/src/backgroundSnapshot.ts (1)
packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts (2)
  • __globalSnapshotPatch (58-58)
  • SnapshotOperation (11-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Playwright (Default) / check
🔇 Additional comments (18)
packages/react/runtime/src/lifecycle/patch/updateMainThread.ts (1)

14-14: LGTM: Proper integration of deferred snapshot cleanup.

The addition of destroyRemovedSnapshotInstances() before __FlushElementTree ensures that stale snapshot instances are cleaned up at the appropriate time in the render cycle, which is essential for the new Suspense implementation's snapshot lifecycle management.

Also applies to: 54-54

packages/react/runtime/src/internal.ts (1)

13-13: Verify Suspense API compatibility with Preact/React

Please ensure that switching from preact/compat’s Suspense to our internal implementation maintains the same public API:

  • File under review: packages/react/runtime/src/internal.ts at the import on line 13
    - import { Suspense } from 'preact/compat';
    + import { Suspense } from './lynx/suspense.js';
  • Confirm that our internal Suspense (in packages/react/runtime/src/lynx/suspense.ts):
    • Exports a FunctionComponent named Suspense
    • Accepts the same props as Preact/React’s Suspense (especially children and fallback)
    • Has the same fallback type (e.g. optional vs. required, accepts arrays or strings)
    • Behaves identically when rendering and unmounting (snapshot lifecycle)

No changes to the API signature or behavior should be introduced.

packages/react/runtime/__test__/debug/formatPatch.test.ts (1)

25-26: LGTM: Comprehensive test coverage for PreventDestroy operation.

The test properly covers the new PreventDestroy snapshot operation with both input format (operation code + ID) and expected formatted output, ensuring the debug formatting function works correctly.

Also applies to: 45-45

packages/react/runtime/src/index.ts (1)

36-36: LGTM: Consistent switch to internal Suspense implementation.

The import change properly exposes the new internal Suspense component through the main export, ensuring all consumers get the enhanced functionality for supporting host elements as direct children.

.changeset/fast-coats-swim.md (1)

1-6: LGTM: Accurate changeset entry.

The changeset correctly categorizes this as a patch version update for @lynx-js/react and accurately describes the new feature allowing host elements as direct children of Suspense.

packages/react/runtime/__test__/renderToOpcodes.test.jsx (1)

686-687: Great to flush pending destructions

Calling destroyRemovedSnapshotInstances() here makes the assertion deterministic.
Just make sure the same call is present in the suite’s afterEach, otherwise later tests may still clear without teardown.

packages/react/runtime/__test__/preact.test.jsx (1)

177-178: Good explicit cleanup, but still clear the queue for following tests

Same remark: ensure this call (or a global hook) precedes any direct snapshotInstanceManager.clear() to preserve tearDown() semantics.

packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx (1)

37-44: Snapshot update reflects new Suspense wrapper

Snapshots now show the internal <wrapper> introduced by the custom Suspense implementation – looks correct.

Also applies to: 52-59

packages/react/runtime/src/backgroundSnapshot.ts (2)

41-44: LGTM! The conditional logic aligns with the Suspense implementation.

The exclusion of 'div' elements from the snapshot patch is appropriate for the new Suspense implementation, which uses wrapper elements that should not be created in the main thread.


78-80: No usages of BackgroundSnapshotInstance.appendChild found

A full search of .ts/.tsx/.js/.jsx files revealed no calls to appendChild on BackgroundSnapshotInstance outside its own definition. Disabling this method has no current impact on the codebase.

packages/react/runtime/src/lifecycle/patch/snapshotPatch.ts (1)

17-17: LGTM! The PreventDestroy operation is properly integrated.

The new operation follows the established pattern with sequential numbering and appropriate parameter definition.

Also applies to: 35-38

packages/react/runtime/src/lifecycle/patch/snapshotPatchApply.ts (1)

92-101: LGTM! Consistent implementation with proper error handling.

The PreventDestroy case follows the established pattern for snapshot operations with appropriate error handling when the context is not found.

packages/react/runtime/src/lynx/suspense.ts (2)

18-19: LGTM! Appropriate thread-aware createElement selection.

The conditional selection of createElement based on the thread context ensures proper behavior in both main and background threads.


33-42: No race condition in the fallback ref callback
The fallback ref handler explicitly checks childrenRef.current and only runs when it’s been set, so it safely no-ops if the fallback mounts before the children. Preact’s Suspense update sequence (as exercised by the existing suspense.test.jsx) ensures that after hydration the child wrapper ref fires before the next fallback mount, and the test suite confirms that the PreventDestroy patch is applied at the correct time.

packages/react/runtime/src/snapshot.ts (1)

407-416: Good improvement to cleanup logic

The use of delete operator for property removal and the cleanup from snapshotInstanceManager.values during traversal ensures proper memory cleanup.

packages/react/runtime/__test__/lynx/suspense.test.jsx (3)

214-412: Excellent test coverage for the main feature

This test case thoroughly validates the PR's main objective of supporting host elements as direct children of Suspense. The verification of the PreventDestroy operation in the snapshot patch is particularly important for ensuring the deferred destruction mechanism works correctly.


1-1629: Comprehensive and well-structured test suite

This test file provides excellent coverage for the new Suspense implementation. The tests are well-organized, clearly documented, and thoroughly validate the integration with the snapshot lifecycle management system, including the new PreventDestroy operation and deferred destruction mechanism.


41-41: Verify Promise.withResolvers() Compatibility

Ensure your test environment supports the modern Promise.withResolvers() API (ES2024) or include a polyfill:

• Browser support starts at Chrome 119, Edge 119, Firefox 121, Safari 17.4, Opera 105 (no support in IE).
• Node.js support begins in v22.0.0; older versions will throw “Promise.withResolvers is undefined.”

If your CI or local setup targets Node < 22 or browsers below these versions, add a polyfill (e.g., core-js or a small custom shim) to avoid test failures.

@Yradex Yradex marked this pull request as draft July 22, 2025 08:29
@Yradex Yradex force-pushed the suspense/element-in-suspense branch 5 times, most recently from b259641 to b702631 Compare July 23, 2025 07:34
@Yradex Yradex marked this pull request as ready for review July 23, 2025 07:38
@Yradex Yradex force-pushed the suspense/element-in-suspense branch from b702631 to 231a08f Compare July 23, 2025 08:26
@Yradex Yradex merged commit 6657da6 into lynx-family:main Aug 6, 2025
45 checks passed
@Yradex Yradex deleted the suspense/element-in-suspense branch August 6, 2025 03:20
Yradex added a commit that referenced this pull request Aug 6, 2025
Yradex added a commit to Yradex/lynx-stack that referenced this pull request Aug 7, 2025
…Suspense (lynx-family#1308)

## Summary

Support using a host element as direct child of Suspense.

## Checklist

- [x] Tests updated (or not required).
- [ ] Documentation updated (or not required).


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved Suspense component now allows direct use of host elements
(such as DOM elements) as children.
* **Bug Fixes**
* Enhanced lifecycle management to ensure proper cleanup and restoration
of elements during Suspense operations.
* Introduced explicit tracking and reconstruction of removed nodes to
improve rendering consistency.
* **Tests**
* Added comprehensive tests covering Suspense behaviors, including
fallback rendering, cleanup, nested and parallel Suspense, error
boundaries, and resolved promises.
* **Style**
* Updated test snapshots to reflect new wrapper structure in rendered
output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants