Skip to content

feat(🐎): drive multiple animated props from a single shared value (select)#3899

Open
Grassper wants to merge 8 commits into
Shopify:mainfrom
Grassper:main
Open

feat(🐎): drive multiple animated props from a single shared value (select)#3899
Grassper wants to merge 8 commits into
Shopify:mainfrom
Grassper:main

Conversation

@Grassper

Copy link
Copy Markdown

What problem this solves

Today every animated Skia prop must be backed by its own shared or derived value. When many props derive from one source — e.g. several elements driven by a single real-time data stream — this has a few costs:

  • Subscription overhead. Each useDerivedValue is its own Reanimated mapper running on the UI thread. N props → N subscriptions, even when they all come from the same source.
  • Scattered state. A component's animated values are spread across many separate hooks instead of living together, which is harder to read and evolve.
  • Maintainability. Adding or changing an animated field means wiring up another hook; the per-prop boilerplate grows with the component.
  • Accidental derived-value chaining. With many interdependent derived values it's easy to derive one from another, creating chains that recompute in sequence and are hard to reason about.

select addresses all of these by letting a single shared value — whose value is an object — drive many props, one key each. The animated state is grouped in one place, Reanimated subscribes once regardless of how many props read from it, adding a field is just adding a key, and there's a single source of truth with no derived-value chains.

What changed

select(sharedValue, key) binds a prop to a single key of a "grouped" shared value (one whose .value is an object):

const circle = useSharedValue({ cx: 0, cy: 0, r: 10 });
// one shared value, one subscription, three props:
<Circle cx={select(circle, "cx")} cy={select(circle, "cy")} r={select(circle, "r")} />

Public API

  • select(sharedValue, key) — creates the binding (typed: a wrong key/value type is a compile error for concrete object types)
  • SharedValueSelector<T> — added to the AnimatedProp<T> union
  • isSharedValueSelector — runtime guard

Implementation

  • Native (Convertor.h): resolves sharedValue.value[key] per prop on the UI thread; ReanimatedRecorder registers the underlying shared value once (deduped) even when many props select from it.
  • Web (Recorder.processProps + materializeCommand): same behaviour on the JS path — collect/dedup the shared value, resolve the key each frame.

Both paths skip transient non-applicable values (null/undefined, and an un-ticked animation function) rather than crashing; genuine type mismatches still surface as errors.

How to test

  • Example: Example app → Reanimated → "Grouped vs. per-prop animation values" (SharedValueComparison). The same 72-prop animation is wired two ways; the toggle flips between 1 Reanimated mapper (grouped) and 72 (per-prop).
  • Unit: in packages/skia, run yarn test src/sksg/__tests__/SharedValueSelector.spec.ts — covers the guard, select, web materializeCommand resolution, and recorder dedup.

Notes / limitations

  • Reanimated only animates values assigned directly to .value, not values nested inside an object, so per-key withTiming/withSpring ({ cx: withTiming(100) }) is not supported — assign plain values to the object, or build it in a useDerivedValue. Documented in the Animations page.
  • Test-coverage boundary: the guard and the full web path are unit-tested. The native ReanimatedRecorder and Convertor.h aren't unit-testable in the jest suite (Skia.Recorder() is native-only; there's no C++ test harness) — they share the same dedup/guard logic and were verified manually via the example.

Video example

groupedSharedVallue.mov

Grassper added 7 commits June 20, 2026 18:37
…value

Previously each animated prop required its own shared value. This adds a
wrapper ({ __sv, __key }) so a single shared value whose `.value` is an
object can drive multiple props, each reading one key. The Reanimated
recorder registers the underlying shared value once (deduped on __sv), and
the native converter resolves `sharedValue.value[__key]` per prop on the UI
thread.
@Grassper Grassper marked this pull request as ready for review June 21, 2026 15:49
@Grassper

Copy link
Copy Markdown
Author

Ran e2e locally on iOS — 399 pass. The one failure (Snapshot2) is an unrelated full-screen snapshot that differs due to my local simulator version, not this change.

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