Skip to content

Add createAnimatedRestyleComponent and a guide for animating Restyle components (Reanimated 4.4+) - #358

Open
tothvoj-gl wants to merge 1 commit into
Shopify:masterfrom
tothvoj-gl:feature/355-animated-restyle-component
Open

tothvoj-gl wants to merge 1 commit into
Shopify:masterfrom
tothvoj-gl:feature/355-animated-restyle-component

Conversation

@tothvoj-gl

Copy link
Copy Markdown

Problem

react-native-reanimated 4.4+ ships a default-on flag, FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS. Once an animation settles, Reanimated re-renders the component it wraps with the resolved style values spread on as top-level props, in addition to style. If that wrapped component is a Restyle component built as Animated.createAnimatedComponent(createBox()), those raw values land as theme-key props on it, and getThemeValue throws:

Value '13' does not exist in theme['borderRadii']
Value 'rgba(255,209,102,1)' does not exist in theme['colors']

This is reported in #355 (two independent reports). #356 (@KAMRONBEK) fixes this at the getThemeValue level, by passing raw numeric and color values through instead of throwing. This PR is a different, complementary fix that addresses the underlying ordering cause of the collision rather than getThemeValue's strictness, and doesn't touch getThemeValue.ts or anything else #356 touches.

Cause

The crash is really an ordering problem. Given:

// A — crashes
const AnimatedBox = Animated.createAnimatedComponent(createBox<Theme>());

Reanimated's settle mechanism re-renders whatever it directly wraps — here, the Restyle component itself — spreading the settled props onto it, where Restyle's prop parsing sees and validates them.

// B — doesn't crash
const AnimatedBox = createAnimatedRestyleComponent<Theme, typeof Animated.View>(
  Animated.createAnimatedComponent(View),
);

Here Reanimated wraps a plain View inside Restyle's BaseComponent slot. The settled props are spread onto that inner View — a component Restyle's prop parsing never sees. I verified this empirically against real react-native-reanimated 4.5.1 in a scratch app: ordering A crashes exactly as in #355; ordering B, animating the same borderRadius/backgroundColor/zIndex values, settles with zero errors.

I also checked:

  • createRestyleComponent already forwards refs (React.forwardRef → BaseComponent) — confirmed both by reading the source and empirically, with useAnimatedRef + measure() returning the correct layout through the ref chain. No ref work was needed.
  • Variants and responsive props compose fine under ordering B, the same as any custom component built with createRestyleComponent.
  • AnimatePresence Giving Error  #172 ("AnimatePresence Giving Error") is not the same root cause — it's a Framer Motion (web) + TypeScript prop-type issue, unrelated to Reanimated. Not claiming it here, since it isn't related.
  • AnimatedBox predefined component #230 ("AnimatedBox predefined component") is people hand-rolling ordering B already, with no official API — this PR gives that pattern a name and proper type inference.

Fix

Since ordering B already works with Restyle's existing createBox/createRestyleComponent (both already accept an arbitrary BaseComponent), no interception layer or runtime behavior change was needed. This PR adds:

  • createAnimatedRestyleComponent: a small wrapper around createBox that infers Props from the animated component you hand it (so its style prop type doesn't need to be redeclared by hand), instead of requiring the createBox<Theme, Props>(BaseComponent) boilerplate directly. Restyle still never imports an animation library — the caller constructs the animated component and hands it in.
  • An "Animating Restyle components" guide explaining the ordering issue and the new helper.
  • A CHANGELOG entry.

What this deliberately does not change

  • getThemeValue.ts and anything else Pass through raw numeric and color style values instead of throwing when not found in theme #356 touches — untouched. I cherry-picked Pass through raw numeric and color style values instead of throwing when not found in theme #356's commit onto this branch to check: it applies with only a trivial conflict in the shared CHANGELOG "Next" section, and the combined test suite (53 + 7 = 60 tests) passes.
  • No new dependency on react-native-reanimated (or any animation library) anywhere, including in this new file — it's generic over any already-animated component the caller supplies.
  • No change to BoxProps, theme typing, or any existing component's behavior — createAnimatedRestyleComponent is a new, additive export; existing createBox/createText/createRestyleComponent consumers are unaffected (full existing suite is still green).
  • backgroundColor="nonsense" (a genuine typo'd theme key) still fails at compile time and throws at runtime — this PR doesn't touch that constraint, and it's covered by a test.

Test plan

New tests in createAnimatedRestyleComponent.test.tsx (using a mock animated component that reproduces Reanimated's actual settle mechanism — an internal setState that spreads resolved values onto its own rendered child — so the suite doesn't need react-native-reanimated as a dependency):

  • Settled animation spreads a raw color value → no throw, correct value reaches the underlying view
  • Settled animation spreads raw borderRadius/zIndex → no throw
  • Theme tokens still resolve normally (backgroundColor="primary", etc.)
  • A genuine typo'd theme key still throws
  • Variants compose correctly when explicitly added via createRestyleComponent (same as any custom component)
  • Responsive props work
  • Refs forward to the wrapped animated component

Also manually verified end-to-end against real react-native-reanimated 4.5.1 + react-native-worklets 0.10.4 on iOS (RN 0.86.3), using the actual proposed API — animated backgroundColor/borderRadius settled correctly with zero errors.

yarn test: 51/51 passing (44 existing + 7 new). yarn lint: clean. yarn build: clean.

Refs #355, Refs #230

🤖 Generated with Claude Code

@tothvoj-gl

Copy link
Copy Markdown
Author

I have signed the CLA!

Reanimated 4.4+'s default-on FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS
flag re-renders a wrapped component with its settled style values
spread as top-level props. If that wrapped component is a Restyle
component (Animated.createAnimatedComponent(createBox())), those raw
values land in Restyle's theme-token prop namespace and getThemeValue
throws (Shopify#355).

Nesting the other way around - Restyle wrapping the already-animated
component (createAnimatedRestyleComponent(Animated.createAnimatedComponent(View)))
- avoids the collision entirely: the settled props land on the plain
view Reanimated wraps internally, which Restyle's prop parsing never
sees. No interception layer is needed; this is a design-spike finding,
verified against real react-native-reanimated 4.5.1 in a scratch app
(animated color, borderRadius and zIndex all settle correctly with
zero errors) as well as with the unit tests added here.

createAnimatedRestyleComponent is a thin wrapper around createBox that
infers Props from the animated component handed to it, so its own
prop types (style in particular) don't need to be redeclared by hand.
Restyle still never imports an animation library - the caller
constructs the animated component and hands it in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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