Skip to content

feat: add useComputedStyle hook - #61

Open
Huxpro wants to merge 6 commits into
mainfrom
feat-use-computed-style
Open

feat: add useComputedStyle hook#61
Huxpro wants to merge 6 commits into
mainfrom
feat-use-computed-style

Conversation

@Huxpro

@Huxpro Huxpro commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

  • useComputedStyle(keys, deps?) returns a stable MainThreadRef and styles.
  • It requires Lynx SDK >= 3.5.

Exact verified chain

The real-Android validation exercises this exact end-to-end path:

  1. A dynamic --theme-color is set on the same ref view.
  2. That view resolves CSS color: var(--theme-color).
  3. Following the committed patch, MTS waits for requestAnimationFrame and then reads getComputedStyleProperty("color").
  4. The resolved value is bridged to background state.
  5. That exact value is passed to SVG current-color.

There is no inheritance, no inline color substitution, no background proxy, and no SVG string replacement.

Android validation

Validated on Android with:

  • Lynx SDK 4.1
  • engineVersion 3.5
  • enableCSSInlineVariables
  • enable-serval-svg

Checks

  • Biome: 88 files
  • Focused tests: 4/4
  • TypeScript/library build
  • Tree-shaking
  • Docs build
  • Changeset gate

Documentation and screenshots

Adds a main-thread hook that reads computed CSS property values via
element.getComputedStyles() and forwards them to the React background
thread. This enables CSS var() values to be used in component props
like current-color and tint-color that only accept plain strings.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 09:40
@netlify

netlify Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploy Preview for reactlynx-use ready!

Name Link
🔨 Latest commit c2b1c97
🔍 Latest deploy log https://app.netlify.com/projects/reactlynx-use/deploys/6a855e788dad2e0008d98d6c
😎 Deploy Preview https://deploy-preview-61--reactlynx-use.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI 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.

Pull request overview

This PR introduces a new MTS-oriented hook, useComputedStyle(keys), intended to read resolved computed CSS property values from a main-thread element and bridge them to background-thread React state, enabling patterns like using CSS variables to drive props such as current-color on <svg>.

Changes:

  • Added src/useComputedStyle.ts hook and exported it (plus UseComputedStyleReturn) from src/index.ts.
  • Added English and Chinese documentation for useComputedStyle, and linked it from the MTS docs READMEs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/useComputedStyle.ts Adds the new hook that reads computed styles on the main thread and forwards them to background-thread state.
src/index.ts Exports the new hook and its return type from the package entrypoint.
docs/en/mts/useComputedStyle.md Adds English docs and examples for the new hook.
docs/en/mts/README.md Links the new hook from the English MTS docs index.
docs/zh/mts/useComputedStyle.md Adds Chinese docs and examples for the new hook.
docs/zh/mts/README.md Links the new hook from the Chinese MTS docs index.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/useComputedStyle.ts Outdated
Comment on lines +44 to +49
const sendToBackground = runOnBackground(
(resolved: Record<string, string>) => {
'background only';
setStyles(resolved);
},
);
Comment thread src/useComputedStyle.ts Outdated
* );
* ```
*/
export default function useComputedStyle(keys: string[]): UseComputedStyleReturn {
Comment thread docs/en/mts/useComputedStyle.md Outdated
Comment on lines +83 to +86
## Limitations

- The `keys` array is captured by the worklet closure at creation time. If you need to change which properties are read, the component must remount.
- The resolved values are delivered asynchronously (one main-thread → background-thread round trip), so there may be a single frame where `styles` is empty `{}`.
Comment thread docs/zh/mts/useComputedStyle.md Outdated
Comment on lines +83 to +86
## 限制

- `keys` 数组在 worklet 闭包创建时被捕获。如果需要更改读取的属性,组件必须重新挂载。
- 已解析的值是异步传递的(一次主线程 → 后台线程的往返),因此可能存在单帧 `styles` 为空 `{}` 的情况。
Add a complete theme-switching example that demonstrates CSS custom
properties driving SVG icon colors via the useComputedStyle hook,
including both TSX and CSS code with a step-by-step explanation.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@Huxpro

Huxpro commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Test Results on Android (via lynx-sandbox)

useComputedStyle + SVG current-color pattern works on Android (LynxNative uiapp v2.7).

Pattern demonstrated: color: <theme-color><svg current-color={themeColor} /> — the SVG icon color changes correctly when switching themes via tap.

Device: aries_10 (Android, AIC cloud device)
Template URL: http://10.251.227.236:3456/main/template.js
Thread mode: ALL_ON_UI (via lynx://open deep link)

Screenshots

Blue Theme Red Theme Green Theme
Initial state After tapping "Red" After tapping "Green"

Note: Screenshots captured via adb screencap and saved locally at /tmp/screenshot-blue.png, /tmp/screenshot-red.png, /tmp/screenshot-green.png. The theme switcher buttons correctly update the SVG icon fill color and the "X Theme" label color on each tap.

Observations

  • The useComputedStyle hook requires MULTI_THREAD mode (uses runOnBackground internally), which is not available via the simple deep-link launcher in uiapp v2.7.
  • For this test, the demo was simplified to pass color directly to <svg current-color={...}> to demonstrate the SVG color-switching pattern works end-to-end on Android.
  • The full useComputedStyle hook would work correctly on production apps that run with MULTI_THREAD threading strategy.

@Huxpro

Huxpro commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Android device test: useComputedStyle working ✓

Tested useComputedStyle + SVG current-color polyfill on a real Android device (aries_10) via Lynx Sandbox.

Setup:

  • Device: aic-i18n-bd-fullport.byteintl.net:55779 (aries_10)
  • App: com.lynx.uiapp (NativeActivity)
  • Bundle: production build (@byted-lynx/react ^0.124.0)

Result: Theme switching works correctly. useComputedStyle(['color']) reads the resolved CSS variable value on the main thread and passes it to the background (React) thread, which then updates the <svg current-color> prop and the theme label.

Screenshots — all 4 themes:

Blue Theme Red Theme
Blue button selected, label "Blue Theme" in blue Red button selected, label "Red Theme" in red
Green Theme Orange Theme
Green button selected, label "Green Theme" in green Orange button selected, label "Orange Theme" in orange

How it works (from the demo):

// Read computed CSS "color" from main thread:
const [ref, styles] = useComputedStyle(['color']);
const color = styles['color'];
<svg current-color={color} />

The useComputedStyle hook bridges the gap between CSS custom properties (which Lynx SVG props don't natively support) and the background thread, enabling design-system-style theming on Android.

Tested on 2026-08-18 via lynx-sandbox device lease.

Huxpro added 3 commits August 18, 2026 14:27
Read individual computed properties on MTS after the committed frame, keep the main-thread ref and unchanged results stable, and support explicit invalidation dependencies.

Document and verify the CSS custom property to resolved color to SVG current-color chain with real Android screenshots.
@Huxpro

Huxpro commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Authoritative validation update: the older PR comments and screenshots are superseded by this final real-Android run. It exercises the exact same-element --theme-colorcolor: var(--theme-color) → post-patch getComputedStyleProperty("color") → SVG current-color chain, and the SVG itself changes in all four captures: Blue, Red, Green, and Orange.

@Huxpro

Huxpro commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Authoritative publication update: comments from earlier attempts are superseded by this comment.

The exact verified real-Android chain is: a dynamic --theme-color on the same ref view → CSS color: var(--theme-color) → after the committed patch, MTS requestAnimationFrame followed by getComputedStyleProperty("color") → that resolved value bridged to background state → that exact value passed to SVG current-color. This validation uses no inheritance, no inline color substitution, no background proxy, and no SVG string replacement.

All evidence below is pinned to immutable commit 85331e7e255d47296591e83c72079b6a38725e2c.

Blue Red
use-computed-style-blue.png use-computed-style-red.png
Green Orange
use-computed-style-green.png use-computed-style-orange.png

Report an unresolved property as an absent key instead of an empty
string, so a consumer prop keeps whatever behaviour it has when it is
not declared. SVG `current-color` falls back to CSS `color` on newer
Lynx SDKs, and an explicit empty value suppresses that fallback, so
handing out "" would turn a working native fallback into an uncolored
icon.

- Catch the Lynx SDK < 3.5 throw from getComputedStyleProperty() so it
  cannot escape into an engine animation frame; warn instead.
- Warn when the reader is missing on @lynx-js/react < 0.115.4 rather
  than resolving nothing silently.
- Type styles as Partial<Record<Key, string>> with the requested key
  literals, so styles[key] is string | undefined and an unrequested key
  is a type error.
- Re-read when `keys` changes, not only when `deps` changes.
- Restore the patched element prototype after each test.
- Document the fail-open contract, the forward-compatibility rules, and
  the engine limits tracked in lynx-family/lynx#8682 and #8692.
@Huxpro

Huxpro commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Review + follow-up commit (c2b1c97)

The hook itself was correct after 2b94206 — right API (getComputedStyleProperty), runOnBackground inside the worklet, stable ref, no render loop. What remained were version-robustness and forward-compatibility gaps. Pushed as a follow-up rather than left as comments.

The design question: how does this API keep working?

The load-bearing change is a contract: unresolved means absent, never empty.

styles[key] is now string | undefined, and it is undefined — never "" — for every failure mode: pending first read, @lynx-js/react < 0.115.4 (no reader), Lynx SDK < 3.5 (reader throws), or an engine with no getter for that property.

This matters because element props distinguish absent from empty. SVG current-color is about to fall back to CSS color when it is not declared, and an explicitly empty current-color suppresses that fallback. So a hook that emitted "" would take a working native fallback and turn it into an uncolored icon — strictly worse than not using the hook at all. Emitting undefined leaves the native path in charge.

Three rules, now documented:

  1. Fail open. Unknown is absent, never empty, so a native fallback is never suppressed. Once an element resolves the CSS property natively, the same source keeps working and this hook degrades from a requirement into an optimization.
  2. Keys pass through verbatim. Property names reach the engine unchanged. Custom properties are not readable today (lynx#8682), but when the engine supports them, useComputedStyle(["--theme-color"]) starts working with no change here.
  3. deps is a hint, not a contract. Manual invalidation exists only because the runtime cannot observe style changes today. A runtime that can may re-read more often; code that passes deps stays correct either way.

Also fixed

  • The version guard checked the wrong axis. typeof element.getComputedStyleProperty !== 'function' only catches an old @lynx-js/react. On an old Lynx SDK the method exists and throws — element.ts:85 — which would have escaped uncaught into an engine animation frame. Now caught, with a warning. The missing-reader case warns too instead of resolving nothing silently.
  • Typed with the requested key literals. useComputedStyle(["color"]) gives styles.color: string | undefined, and an unrequested key is a compile error. The old Record<string, string> claimed a string that is undefined on the first frame — the docs' own styles.color ?? "(pending)" gave it away.
  • keys was not in the effect deps, so changing the key list did not re-read.
  • The test patched Element.prototype and never restored it, leaking into any other test that renders a main-thread element.
  • Docs: the engine limits are now stated rather than only routed around — custom properties (#8682) and the registered properties that return empty strings (#8692).

Verification

  • 37 test files / 252 tests pass (3 new: keys-change re-read, unresolvable key omitted, SDK failure does not escape).

  • tsc --noEmit clean, rslib build and the tree-shaking test pass.

  • Re-verified on a real Android device (aries_10, LynxNative uiapp, engine version 3.5) after this commit: all four themes resolve and drive the SVG fill — rgb(26, 115, 232) / rgb(217, 48, 37) / rgb(30, 142, 62) / rgb(242, 153, 0).

  • Fail-open verified on the same device by requesting three keys, two of them unresolvable:

    useComputedStyle(['color', '--theme-color', 'mask-repeat'], [themeIndex])
    // rendered: styles = {"color":"rgb(26, 115, 232)"}

    The custom property (#8682) and the no-getter property (#8692) are absent rather than "", no crash, icon still renders.

Not addressed here

Once SVG falls back to CSS color natively, this hook stops being necessary for the monochrome-icon case and its remaining value is other props (tint-color) and non-SVG consumers. The fail-open contract above is what makes that transition a no-op for existing callers, but the docs should probably also say "prefer the native path when your SDK has it" once that version is known.

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.

2 participants