feat(🐎): drive multiple animated props from a single shared value (select)#3899
feat(🐎): drive multiple animated props from a single shared value (select)#3899Grassper wants to merge 10 commits into
Conversation
…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.
|
Ran e2e locally on iOS — 399 pass. The one failure ( |
|
This is looks very good and the idea is sound. |
|
Thanks @wcandillon! That actually matches my experience — the raw frame-time delta is modest, so I'd treat the subscription reduction as a secondary benefit rather than the headline. The primary motivation is API design and maintainability: grouping related values by domain concept (price, change, change %) instead of wiring a separate derived value per prop. It keeps a component's animated state in one place and avoids accidental derived-value chaining. Concrete example — the production trading terminal I built this for: each row animates price, change, and change %, all updating on the same tick. At 20 rows that's ~60 derived-value subscriptions vs 20 grouped shared values. The per-frame work is similar, but there are far fewer subscriptions/worklets to set up and maintain. Happy to put together a formal benchmark if that'd help quantify it! |
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:
useDerivedValueis its own Reanimated mapper running on the UI thread. N props → N subscriptions, even when they all come from the same source.selectaddresses 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.valueis an object):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 theAnimatedProp<T>unionisSharedValueSelector— runtime guardImplementation
Convertor.h): resolvessharedValue.value[key]per prop on the UI thread;ReanimatedRecorderregisters the underlying shared value once (deduped) even when many props select from it.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
SharedValueComparison). The same 72-prop animation is wired two ways; the toggle flips between 1 Reanimated mapper (grouped) and 72 (per-prop).packages/skia, runyarn test src/sksg/__tests__/SharedValueSelector.spec.ts— covers the guard,select, webmaterializeCommandresolution, and recorder dedup.Notes / limitations
Reanimated only animates values assigned directly to
.value, not values nested inside an object, so per-keywithTiming/withSpring({ cx: withTiming(100) }) is not supported — assign plain values to the object, or build it in auseDerivedValue. Documented in the Animations page.Test-coverage boundary: the guard and the full web path are unit-tested. The native
ReanimatedRecorderandConvertor.haren'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.When to use it:
selectis best when several props share one source that updates together (e.g. a single data tick). Because any key change re-applies every prop bound to that value, props that change independently — or very large objects — may be better served by separate shared values.Video example
groupedSharedVallue.mov