Skip to content

fix(react): wrap <wrapper> for JSX with dynamic key #1541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 19, 2025

Conversation

colinaaa
Copy link
Collaborator

@colinaaa colinaaa commented Aug 15, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Fixed incorrect render results when using expressions as keys, improving consistency for dynamic-key scenarios, slot wrapping, and list-item children with dynamic parts.
  • Tests

    • Updated snapshot expectations and cleaned up test titles to align with corrected rendering behavior; removed unused imports.
  • Chores

    • Added changeset for a patch release of @lynx-js/react (lynx-stack#1371).

This partially reverts #547.

close: #1371

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

Copy link

changeset-bot bot commented Aug 15, 2025

🦋 Changeset detected

Latest commit: a1c4fe8

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

Copy link
Contributor

coderabbitai bot commented Aug 15, 2025

📝 Walkthrough

Walkthrough

The PR updates the JSX transform to alter wrapper/merge behavior for dynamic keys and adjusts related test snapshots across runtime and testing-library suites. A changeset marks a patch release for @lynx-js/react referencing issue #1371. No exported/public APIs are changed.

Changes

Cohort / File(s) Summary
Release metadata
.changeset/modern-bags-wait.md
Adds a patch changeset for @lynx-js/react noting a fix for incorrect render results when using expression as key (refs #1371).
Runtime tests (React)
packages/react/runtime/__test__/basic.test.jsx, packages/react/runtime/__test__/preact.test.jsx, packages/react/runtime/__test__/renderToOpcodes.test.jsx
Updates inline snapshots and test names to match new wrapper/slot structure and snapshot token IDs; removes an unused import; replaces random key with deterministic random prop in an opcode test.
Testing-library tests
packages/react/testing-library/src/__tests__/list.test.jsx, packages/react/testing-library/src/__tests__/render.test.jsx
Revises snapshots to reflect wrapper inserted around list-item children and reflowed structure for dynamic key/slot scenarios; cleans up test titles.
JSX transform (Rust)
packages/react/transform/src/swc_plugin_snapshot/slot_marker.rs
Changes merging to ignore dynamic-key status for non-custom elements; adds has_dynamic_key gating for wrapper creation (wrap on is_list or has_dynamic_key when children exist).
Transform snapshots
packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/should_wrap_dynamic_key.js, packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/slot_marker.rs/should_wrap_dynamic_part.js
Updates snapshot structures to align with new dynamic-key wrapper and internal-slot organization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix incorrect render when using key with multiple children; avoid multiple __DynamicPartChildren in a slot (#1371)

Assessment against linked issues: Out-of-scope changes

(None)

Possibly related PRs

Suggested labels

framework:React

Suggested reviewers

  • hzy

Poem

I hop through slots with careful cheer,
Wrapping keys that once were queer.
No twins of DynamicPart appear—
The views align, the path is clear!
Thump-thump goes CI’s happy beat,
Patchy paws make renders neat. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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.
    • 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.
  • 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 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/Issue comments)

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

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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

Copy link

codecov bot commented Aug 15, 2025

Codecov Report

❌ Patch coverage is 42.85714% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...napshot/slot_marker.rs/should_wrap_dynamic_part.js 0.00% 3 Missing ⚠️
..._plugin_snapshot/mod.rs/should_wrap_dynamic_key.js 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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: 0

🧹 Nitpick comments (8)
packages/react/runtime/__test__/renderToOpcodes.test.jsx (1)

147-152: Stabilize the “random” value to avoid accidental test flakiness

Using Math.random is fine since you interpolate the same value into both the input and the inline snapshot, but a fixed deterministic value makes intent clearer and prevents subtle issues if this gets refactored. Consider hard-coding the number.

-    const random = Math.random();
+    const random = 0.123456789; // deterministic for stable snapshot

Also applies to: 171-172

packages/react/transform/src/swc_plugin_snapshot/slot_marker.rs (1)

146-153: Wrap children when is_list OR has_dynamic_key to enforce a single dynamic slot

Adding has_dynamic_key and wrapping children into a single JSXExprContainer when either is_list or has_dynamic_key (and children are non-empty) directly addresses #1371 by preventing multiple DynamicPartChildren entries under a keyed parent with multiple children.

A short comment here would help future readers understand the intent:

-      if (is_list || has_dynamic_key) && !n.children.is_empty() {
+      // If the parent is a list or has a dynamic key, wrap all children into one
+      // expression container so downstream passes emit a single dynamic slot.
+      if (is_list || has_dynamic_key) && !n.children.is_empty() {
packages/react/runtime/__test__/preact.test.jsx (2)

352-390: Background snapshots updated: verify brittleness and intent

The background inline snapshots now reflect the new grouping. Given the frequent renumbering of _snapshot* tokens and structural reshuffles, consider reducing brittleness by asserting structure and semantics (presence of a single grouping wrapper and correct child order) instead of exact IDs, where feasible.

Would you like a helper/assertion that walks the background snapshot tree to validate shape (count of wrappers, children order) instead of relying on token IDs?


674-709: Hydration opcodes changed (-88/-89 etc.): confirm opcode map consistency

Hydration patch opcodes changed (e.g., -68/-69 → -88/-89) and associated value positions shifted. Please confirm these constants remain consistent with the opcode map in runtime and any tooling that consumes them. If those numbers are derived, a short comment or constant alias would help readability and future diffs.

packages/react/testing-library/src/__tests__/render.test.jsx (1)

51-90: Dynamic key wrapper moved inside keyed view — regression fixed

The wrapper now sits inside the keyed and groups the dynamic children. This directly addresses the regression described in #1371 by ensuring a single grouped dynamic-children part.

Optional: add a structural assertion to ensure exactly one exists under the keyed view and it contains both children, to guard against future regressions without depending on snapshot IDs.

packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/slot_marker.rs/should_wrap_dynamic_part.js (1)

23-27: Transformer snapshot aligns with wrapper-on-dynamic-key policy

  • Dynamic keyed now wraps the dynamic content in an internal-slot + wrapper, ensuring a single grouped dynamic part.
  • In subsequent cases, keyed is placed directly under internal-slot (no extra wrapper) where appropriate.

These match the updated transform logic and the intent to avoid multiple __DynamicPartChildren under the same slot.

Consider adding one more transformer test where a keyed element has a mix of text and element children to ensure the wrapper consistently groups all siblings in that slot.

packages/react/testing-library/src/__tests__/list.test.jsx (2)

88-89: UID expectation changed — verify rationale

UID advanced from 5 to 6. That’s expected if the additional wrapper participates in UI sign allocation. Just calling it out to confirm it’s deterministic across environments.


386-389: elementID changed 36 → 33 — confirm stability

The elementID in the flush payload changed. Likely due to the additional wrapper affecting ID allocation. Please confirm this remains deterministic under parallel test runs.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe1708 and a1c4fe8.

📒 Files selected for processing (9)
  • .changeset/modern-bags-wait.md (1 hunks)
  • packages/react/runtime/__test__/basic.test.jsx (4 hunks)
  • packages/react/runtime/__test__/preact.test.jsx (11 hunks)
  • packages/react/runtime/__test__/renderToOpcodes.test.jsx (2 hunks)
  • packages/react/testing-library/src/__tests__/list.test.jsx (18 hunks)
  • packages/react/testing-library/src/__tests__/render.test.jsx (8 hunks)
  • packages/react/transform/src/swc_plugin_snapshot/slot_marker.rs (2 hunks)
  • packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/should_wrap_dynamic_key.js (1 hunks)
  • packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/slot_marker.rs/should_wrap_dynamic_part.js (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: upupming
PR: lynx-family/lynx-stack#1502
File: packages/react/testing-library/types/entry.d.ts:71-71
Timestamp: 2025-08-13T09:20:00.936Z
Learning: In lynx-js/react testing library, wrapper components must have children as a required prop because they are always called with `h(WrapperComponent, null, innerElement)` where innerElement is passed as children. The type `React.JSXElementConstructor<{ children: React.ReactNode }>` correctly requires children to be mandatory.
Learnt from: colinaaa
PR: lynx-family/lynx-stack#1500
File: .changeset/curvy-dragons-appear.md:1-5
Timestamp: 2025-08-13T11:50:58.069Z
Learning: In the lynx-family/lynx-stack repository, all packages under packages/react/ should use "lynx-js/react" in changesets since they are all published together under the same package name, regardless of their individual package.json names. This includes packages/react/testing-library and other sub-packages.
📚 Learning: 2025-07-22T09:23:07.797Z
Learnt from: colinaaa
PR: lynx-family/lynx-stack#1330
File: .changeset/olive-animals-attend.md:1-3
Timestamp: 2025-07-22T09:23:07.797Z
Learning: In the lynx-family/lynx-stack repository, changesets are only required for meaningful changes to end-users such as bugfixes and features. Internal/development changes like chores, refactoring, or removing debug info do not need changeset entries.

Applied to files:

  • .changeset/modern-bags-wait.md
📚 Learning: 2025-08-13T11:50:58.069Z
Learnt from: colinaaa
PR: lynx-family/lynx-stack#1500
File: .changeset/curvy-dragons-appear.md:1-5
Timestamp: 2025-08-13T11:50:58.069Z
Learning: In the lynx-family/lynx-stack repository, all packages under packages/react/ should use "lynx-js/react" in changesets since they are all published together under the same package name, regardless of their individual package.json names. This includes packages/react/testing-library and other sub-packages.

Applied to files:

  • .changeset/modern-bags-wait.md
📚 Learning: 2025-08-07T04:00:59.645Z
Learnt from: colinaaa
PR: lynx-family/lynx-stack#1454
File: pnpm-workspace.yaml:46-46
Timestamp: 2025-08-07T04:00:59.645Z
Learning: In the lynx-family/lynx-stack repository, the webpack patch (patches/webpack5.101.0.patch) was created to fix issues with webpack5.99.9 but only takes effect on webpack5.100.0 and later versions. The patchedDependencies entry should use "webpack@^5.100.0" to ensure the patch applies to the correct version range.

Applied to files:

  • .changeset/modern-bags-wait.md
📚 Learning: 2025-08-13T09:23:36.222Z
Learnt from: upupming
PR: lynx-family/lynx-stack#1502
File: packages/react/testing-library/types/entry.d.ts:97-97
Timestamp: 2025-08-13T09:23:36.222Z
Learning: React Testing Library's rerender function accepts React.ReactNode (including strings, numbers, null, etc.), not just React.ReactElement. The typing should match RTL's behavior by using React.ReactNode for maximum compatibility.

Applied to files:

  • packages/react/testing-library/src/__tests__/render.test.jsx
🧬 Code Graph Analysis (2)
packages/react/runtime/__test__/renderToOpcodes.test.jsx (1)
packages/react/transform/src/wasm.js (1)
  • view (24-28)
packages/react/transform/src/swc_plugin_snapshot/slot_marker.rs (1)
packages/react/transform/src/swc_plugin_snapshot/jsx_helpers.rs (2)
  • jsx_is_custom (207-217)
  • jsx_has_dynamic_key (219-239)
🔇 Additional comments (24)
.changeset/modern-bags-wait.md (1)

2-7: Changeset looks correct and scoped to @lynx-js/react

  • Package name matches repo convention for react subpackages.
  • Description concisely ties to issue #1371 and explains user-facing fix.
packages/react/transform/tests/__swc_snapshots__/src/swc_plugin_snapshot/mod.rs/should_wrap_dynamic_key.js (1)

51-51: Snapshot change aligns with single-slot semantics under a dynamic key

Removing the extra wrapper around the inner dynamic element and keeping one wrapper for the free text ensures we don’t produce multiple __DynamicPartChildren entries for a keyed parent with multiple children. This matches the intended fix for #1371.

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

372-398: “multiple slots 0” snapshot now shows a single wrapper under the keyed parent — this is the desired fix

The keyed now contains a single empty wrapper instead of multiple dynamic children slots. This prevents the runtime from seeing multiple __DynamicPartChildren entries, matching the bug’s root cause.


417-423: “multiple slots 2” snapshot update confirms consolidation to one top-level wrapper

With the key applied on the outer node, children are wrapped into a single wrapper slot. This is consistent with the new transform behavior and should render correctly on first paint.


452-456: “multiple slots 3” snapshot preserves existing wrapper under text and adds a single wrapper under the keyed view

This keeps slot boundaries well-defined and avoids duplicated children slots under the keyed element.

packages/react/transform/src/swc_plugin_snapshot/slot_marker.rs (1)

85-86: Merging rule for JSXElement children simplified to ignore dynamic key presence

Setting should_merge = !jsx_is_custom(element) treats host elements as mergeable and custom elements as dynamic boundaries. Given the parent-level handling of dynamic keys introduced below, this simplification is sound and reduces wrapper churn.

If you want extra guardrails, we can add/verify a transform test where a host child has its own dynamic key but the parent also has a dynamic key, ensuring only a single slot is emitted for the parent’s children.

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

283-296: ResultCell: wrapper placement around conditional block looks correct

The wrapper now encloses the conditional description branch only, leaving the rest of the structure untouched. This aligns with the goal of grouping dynamic children under a single wrapper and should prevent multiple dynamic-children parts.


599-616: Lazy test snapshots: wrapper staging between suspense phases looks consistent

The intermediate fallback and eventual resolved state snapshots reflect the staged wrapper behavior. No issues spotted.


647-671: Dual-runtime background snapshot: per-char wrappers and props — sanity check

The per-character keyed text pieces each appear as their own wrapped card, which is consistent with the dynamic-key grouping strategy. Looks good.


711-746: Global snapshot patch expectations updated: verify no-op before init still holds

The takeGlobalSnapshotPatch() expectations are adjusted and remain empty pre-init, as intended. No concerns beyond the opcode-map consistency noted above.

packages/react/testing-library/src/__tests__/render.test.jsx (4)

109-134: Dynamic key with preceding sibling: wrapper placement remains correct

With a sibling Hello {null} before the keyed view, the wrapper is still applied only within the keyed container. Looks good.


137-180: Dynamic key with following sibling: wrapper placement remains correct

The keyed container’s children are grouped, and unrelated trailing content is unaffected. This matches the intended isolation.


198-219: Root keyed container: inner grouping under wrapper is correct

For a keyed root view, the wrapper correctly groups the inner sibling views, preventing multiple dynamic-child parts from leaking out.


222-260: Nested keys: inner keyed child coexists with outer keyed wrapper

The outer keyed view has a wrapper grouping both its children; the inner keyed child remains intact. This ensures stable reconciliation across nested keyed elements with multiple children.

packages/react/testing-library/src/__tests__/list.test.jsx (10)

50-55: List basic: immediate child of list-item is now a wrapper — OK

Each list-item’s immediate child is a wrapper that encloses the prior single . This improves consistency with the dynamic-key grouping behavior.

Also applies to: 69-74, 99-104, 129-134


198-207: update-list-info type IDs shifted — expected churn

Type identifiers moved (…_6 → …_7). Snapshot churn is expected with wrapper insertion. No action needed.


224-239: Complex list-item: wrapper groups both static siblings and dynamic slot

The wrapper now encloses both the two number texts and the “hello” subtree. This ensures a single dynamic part for the item content and should help reuse during list virtualization.


276-297: Reuse path after removal: wrapper maintained; attributes updated correctly

After reusing the previous element for a different item-key, the wrapper and children are intact; only attributes update. This is the expected minimal-diff behavior.


299-360: Attribute set calls: still minimal — good

The number and nature of __SetAttribute calls remain minimal given the reuse. Wrapper insertion did not introduce extra attribute churn beyond the necessary updates.


398-481: Final list snapshot with wrappers per item — consistent across items

All visible items now render with a single wrapper containing their children. The structure is consistent and should be friendlier for hydration and reuse.


568-572: Deferred list-items: dynamic inline numbers are now wrapped — LGTM

The “Item {index+1}” dynamic parts are correctly wrapped, isolating them within the text node. This reduces ambiguity in patching and hydration.

Also applies to: 584-588, 600-604


661-664: Deferred switch test: update-list-info type IDs shifted — as expected

IDs updated to new snapshots for the list-item templates. No issues spotted.


719-722: Unmount-on-reuse test: type IDs advanced — expected

Template IDs changed due to the wrapper adjustments; behavior under reuse still validated by the test.


769-773: Unmount-on-reuse: inline number wrappers — OK

Dynamic numeric segments wrapped in are rendered as expected. This keeps the unmount/reuse logic unaffected while ensuring consistent dynamic-part grouping.

Also applies to: 785-789

Copy link

codspeed-hq bot commented Aug 15, 2025

CodSpeed Performance Report

Merging #1541 will not alter performance

Comparing colinaaa:colin/0815/slot-key (a1c4fe8) with main (3fe1708)

Summary

✅ 10 untouched benchmarks

Copy link

relativeci bot commented Aug 15, 2025

Web Explorer

#4290 Bundle Size — 348.12KiB (0%).

a1c4fe8(current) vs 3fe1708 main#4286(baseline)

Bundle metrics  no changes
                 Current
#4290
     Baseline
#4286
No change  Initial JS 143.37KiB 143.37KiB
No change  Initial CSS 31.84KiB 31.84KiB
No change  Cache Invalidation 0% 0%
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.94% 3.94%
No change  Packages 4 4
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#4290
     Baseline
#4286
No change  JS 233.32KiB 233.32KiB
No change  Other 82.95KiB 82.95KiB
No change  CSS 31.84KiB 31.84KiB

Bundle analysis reportBranch colinaaa:colin/0815/slot-keyProject dashboard


Generated by RelativeCIDocumentationReport issue

Copy link

relativeci bot commented Aug 15, 2025

React Example

#4297 Bundle Size — 237.04KiB (0%).

a1c4fe8(current) vs 3fe1708 main#4293(baseline)

Bundle metrics  no changes
                 Current
#4297
     Baseline
#4293
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 158 158
No change  Duplicate Modules 64 64
No change  Duplicate Code 45.86% 45.86%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#4297
     Baseline
#4293
No change  IMG 145.76KiB 145.76KiB
No change  Other 91.28KiB 91.28KiB

Bundle analysis reportBranch colinaaa:colin/0815/slot-keyProject dashboard


Generated by RelativeCIDocumentationReport issue

@colinaaa colinaaa marked this pull request as draft August 15, 2025 13:00
@colinaaa colinaaa marked this pull request as ready for review August 18, 2025 02:14
@colinaaa colinaaa requested review from Yradex and hzy August 18, 2025 02:14
@colinaaa colinaaa merged commit a753712 into lynx-family:main Aug 19, 2025
46 of 48 checks passed
@colinaaa colinaaa deleted the colin/0815/slot-key branch August 19, 2025 04:06
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.

[Bug]: Using key with multiple children cause wrong render result
2 participants