Skip to content

fix(react): MTF variable capture in class components#2197

Merged
gaoachao merged 2 commits intolynx-family:mainfrom
Yradex:wt/fix-mtf-component-not-updating-20260205
Feb 6, 2026
Merged

fix(react): MTF variable capture in class components#2197
gaoachao merged 2 commits intolynx-family:mainfrom
Yradex:wt/fix-mtf-component-not-updating-20260205

Conversation

@Yradex
Copy link
Copy Markdown
Collaborator

@Yradex Yradex commented Feb 5, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Fixed captured variables in main-thread functions inside class components so they update correctly.
  • Refactor
    • Certain class-level worklets are now exposed via computed accessors instead of initialized fields, which may change initialization timing.
  • Tests
    • Added/updated tests and snapshots covering class methods and class properties capture scenarios.

Problem

Main-thread worklets inside class components could read stale captured values (e.g. this.a in onTapLepus), because:

  • For TransformTarget::JS, worklet ctx objects are later transformed into functions and invoked with this bound to the ctx object, not the component instance.
  • When the transform emits a class field initializer like onTapLepus = { ... a: this.a }, captured values (this.a, or _c for lexical/module vars) are computed once during construction, then remain frozen on the ctx.

This breaks cases like onTapLepus reading the latest this.a / updated module variables.

What changed

  • swc_plugin_worklet: For TransformTarget::JS, non-static class members that are worklets now emit a getter (get onTapLepus() { return { ... } }) when the worklet captures anything (_c / extracted this.xxx / _jsFn), so the ctx object is re-created on each access.
  • Added helper predicates on the identifier collector to detect whether _c / this / _jsFn captures exist.
  • Coverage: applies to both
    • class methods: class A { onTapLepus(e) { "main thread"; ... } }
    • class properties: class A { onTapLepus = (e) => { "main thread"; ... } }
  • Intentionally JS-only: we do not emit getters for LEPUS/MIXED because the lepus registration path writes this["onTapLepus"] = ..., which can conflict with getter-only accessors.

Tests

  • Updated existing snapshots:
    • should_transform_in_class_js
    • should_transform_in_class_property_js
  • Added new snapshot tests to ensure “capture non-this values” also triggers getter:
    • should_transform_in_class_js_capture_values
    • should_transform_in_class_property_js_capture_values

How to review

  1. Start at packages/react/transform/crates/swc_plugin_worklet/lib.rs:
    • WorkletVisitor::visit_mut_class_member:
      • method branch: when/why it becomes get onTapLepus()
      • class property branch: same rule for onTapLepus = (...) => { ... }
  2. Check the new “has captured” predicates in packages/react/transform/crates/swc_plugin_worklet/extract_ident.rs.
  3. Review snapshots under packages/react/transform/crates/swc_plugin_worklet/tests/__swc_snapshots__/lib.rs/ and confirm JS output now uses getter in the capture cases.
  4. Sanity-check that LEPUS output is unchanged for class methods (still a class field worklet ctx object) to avoid runtime assignment issues.

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

@Yradex Yradex requested a review from gaoachao as a code owner February 5, 2026 08:55
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 9d3d256

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
Copy Markdown
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds a patch changeset and three accessor methods to the identifier extractor; refactors class-member worklet transformation to explicitly handle class methods and class properties (including converting fields to getters for JS targets when captures are present) and updates/adds snapshot tests for these code paths.

Changes

Cohort / File(s) Summary
Release documentation
\.changeset/yummy-memes-punch.md
Patch changeset documenting fix for captured variables in main-thread functions within class components.
Identifier extraction utilities
packages/react/transform/crates/swc_plugin_worklet/extract_ident.rs
Added three public accessor methods (has_extracted_values_props, has_extracted_this_props, has_extracted_js_fns) to check presence/content of extracted identifier collections.
Worklet transformation logic
packages/react/transform/crates/swc_plugin_worklet/lib.rs
Refactored visit_mut_class_member to match on ClassMember variants, handle Method and ClassProp specially: run ident collection, compute worklet type, optionally generate dynamic getters for JS targets, transform worklet bodies, and push top-level registration statements.
Test snapshots
packages/react/transform/crates/swc_plugin_worklet/tests/__swc_snapshots__/lib.rs/should_transform_in_class_js.js, .../should_transform_in_class_js_capture_values.js, .../should_transform_in_class_property_js.js, .../should_transform_in_class_property_js_capture_values.js
Updated and added snapshots verifying getter-based transformations for class methods and properties and handling of captured values.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • gaoachao
  • colinaaa
  • upupming

Poem

🐇 I hop through code with nimble paws and cheer,
I turn fields to getters so captures appear,
Values stay fresh on each eager call,
Worklets dance lightly, no pitfalls at all,
A tiny rabbit applauds this fix — hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(react): MTF variable capture in class components' clearly and specifically summarizes the main change: fixing variable capture for main-thread function (MTF) worklets in class components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • 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

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

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 5, 2026

Copy link
Copy Markdown
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: 2

🤖 Fix all issues with AI agents
In `@packages/react/transform/crates/swc_plugin_worklet/lib.rs`:
- Around line 138-149: Clippy flags the redundant .into() call on the BlockStmt
construction assigned to getter_fn.body; remove the unnecessary conversion so
you construct the BlockStmt and provide it in the expected form (i.e., avoid
calling .into() on the BlockStmt literal) while keeping the ReturnStmt.into()
as-is; update the expression that assigns getter_fn.body (the BlockStmt { ...
}.into() portion) to directly use the BlockStmt in the type expected by
getter_fn.body, referencing GetterFn.body, BlockStmt, ReturnStmt, and
worklet_object_expr to locate the code.
- Around line 256-267: The BlockStmt construction used for the class property
getter is being unnecessarily converted with an extra .into() causing a clippy
lint; locate the body: Some(BlockStmt { ... .stmts: vec![ReturnStmt { ... arg:
Some(worklet_object_expr), } .into()], } .into(),) expression and remove the
redundant .into() on the BlockStmt (leave the BlockStmt value or a single
appropriate conversion), so the BlockStmt is not double-converted while keeping
the ReturnStmt and worklet_object_expr usage intact.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 5, 2026

Merging this PR will degrade performance by 8.72%

❌ 1 regressed benchmark
✅ 62 untouched benchmarks
⏩ 3 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
transform 1000 view elements 42.2 ms 46.3 ms -8.72%

Comparing Yradex:wt/fix-mtf-component-not-updating-20260205 (9d3d256) with main (f9f0987)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci bot commented Feb 5, 2026

Web Explorer

#7548 Bundle Size — 383.72KiB (0%).

9d3d256(current) vs f9f0987 main#7546(baseline)

Bundle metrics  no changes
                 Current
#7548
     Baseline
#7546
No change  Initial JS 154.88KiB 154.88KiB
No change  Initial CSS 35.05KiB 35.05KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 8 8
No change  Assets 8 8
No change  Modules 237 237
No change  Duplicate Modules 16 16
No change  Duplicate Code 2.99% 2.99%
No change  Packages 4 4
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7548
     Baseline
#7546
No change  JS 252.83KiB 252.83KiB
No change  Other 95.85KiB 95.85KiB
No change  CSS 35.05KiB 35.05KiB

Bundle analysis reportBranch Yradex:wt/fix-mtf-component-not-...Project dashboard


Generated by RelativeCIDocumentationReport issue

@gaoachao gaoachao merged commit bf1a177 into lynx-family:main Feb 6, 2026
75 of 78 checks passed
colinaaa pushed a commit that referenced this pull request Feb 23, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @lynx-js/motion@0.0.3

### Patch Changes

- Fix an issue that motion/mini will accidentally imports full version,
causing compiling error
([#2204](#2204))

## @lynx-js/react@0.116.3

### Patch Changes

- fix: remove `lynx.createSelectorQuery` deprecated warning in
production
([#2195](#2195))

- Add a DEV-only guard that detects MainThread flush loops caused by
re-entrant MTS handlers.
([#2159](#2159))

This typically happens when a MainThread handler (e.g. event callback or
`MainThreadRef`) performs UI mutations (like `Element.setStyleProperty`,
`setStyleProperties`, `setAttribute`, or `invoke`) that synchronously
trigger a flush which re-enters the handler again.

- Avoid DEV_ONLY_SetSnapshotEntryName on standalone lazy bundle.
([#2184](#2184))

- Add alog and trace for BTS event handlers.
([#2102](#2102))

- fix: Main thread functions cannot access properties on `this` before
hydration completes.
([#2194](#2194))

    This fixes the `cannot convert to object` error.

- Remove element api calls alog by default, and only enable it when
`__ALOG_ELEMENT_API__` is defined to `true` or environment variable
`REACT_ALOG_ELEMENT_API` is set to `true`.
([#2192](#2192))

- fix: captured variables in main thread functions within class
components do not update correctly
([#2197](#2197))

## @lynx-js/rspeedy@0.13.4

### Patch Changes

- Bump ts-blank-space v0.7.0
([#2238](#2238))

- Bump Rsbuild v1.7.3 with Rspack v1.7.5.
([#2189](#2189))

-   Updated dependencies \[]:
    -   @lynx-js/web-rsbuild-server-middleware@0.19.8

## @lynx-js/qrcode-rsbuild-plugin@0.4.5

### Patch Changes

- Only register console shortcuts when running in TTY environments
([#2202](#2202))

## @lynx-js/react-rsbuild-plugin@0.12.8

### Patch Changes

- Updated dependencies
\[[`4240138`](4240138)]:
    -   @lynx-js/react-webpack-plugin@0.7.4
    -   @lynx-js/react-alias-rsbuild-plugin@0.12.8
    -   @lynx-js/use-sync-external-store@1.5.0
    -   @lynx-js/react-refresh-webpack-plugin@0.3.4
    -   @lynx-js/template-webpack-plugin@0.10.3

## @lynx-js/testing-environment@0.1.11

### Patch Changes

- Remove element api calls alog by default, and only enable it when
`__ALOG_ELEMENT_API__` is defined to `true` or environment variable
`REACT_ALOG_ELEMENT_API` is set to `true`.
([#2192](#2192))

## @lynx-js/web-constants@0.19.8

### Patch Changes

-   Updated dependencies \[]:
    -   @lynx-js/web-worker-rpc@0.19.8

## @lynx-js/web-core@0.19.8

### Patch Changes

- fix: avoid error when LynxView is removed immediately after connected
([#2182](#2182))

-   Updated dependencies \[]:
    -   @lynx-js/web-constants@0.19.8
    -   @lynx-js/web-mainthread-apis@0.19.8
    -   @lynx-js/web-worker-rpc@0.19.8
    -   @lynx-js/web-worker-runtime@0.19.8

## @lynx-js/web-core-wasm@0.0.3

### Patch Changes

- Updated dependencies
\[[`e0972ef`](e0972ef),
[`3bc017e`](3bc017e),
[`e2d349e`](e2d349e),
[`d924b6a`](d924b6a)]:
    -   @lynx-js/web-elements@0.11.2
    -   @lynx-js/web-worker-rpc@0.19.8

## @lynx-js/web-elements@0.11.2

### Patch Changes

- Add scrollHeight/scrollWidth getters to XList.
([#2156](#2156))

- Inherit padding styles for x-input elements.
([#2199](#2199))

- Remove the default lazy-loading attribute from x-image elements.
([#2186](#2186))

- Fix x-input number type forwarding to the inner input element.
([#2193](#2193))

## @lynx-js/web-mainthread-apis@0.19.8

### Patch Changes

-   Updated dependencies \[]:
    -   @lynx-js/web-constants@0.19.8

## @lynx-js/web-worker-runtime@0.19.8

### Patch Changes

-   Updated dependencies \[]:
    -   @lynx-js/web-constants@0.19.8
    -   @lynx-js/web-mainthread-apis@0.19.8
    -   @lynx-js/web-worker-rpc@0.19.8

## @lynx-js/react-webpack-plugin@0.7.4

### Patch Changes

- Remove element api calls alog by default, and only enable it when
`__ALOG_ELEMENT_API__` is defined to `true` or environment variable
`REACT_ALOG_ELEMENT_API` is set to `true`.
([#2192](#2192))

## create-rspeedy@0.13.4



## @lynx-js/react-alias-rsbuild-plugin@0.12.8



## upgrade-rspeedy@0.13.4



## @lynx-js/web-core-server@0.19.8



## @lynx-js/web-rsbuild-server-middleware@0.19.8



## @lynx-js/web-worker-rpc@0.19.8

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

3 participants