From 9b99fbd843f490261e04d8cc354ee1aced860c45 Mon Sep 17 00:00:00 2001 From: Jacek Pudysz Date: Wed, 19 Feb 2025 16:37:40 +0100 Subject: [PATCH] feat: prevent flattening style arrays (#7021) ## Summary ### Why While integrating `Unistyles` with `Reanimated`, there is one clash that I would love to resolve. We encourage developers to merge styles using array syntax, as it makes it easier to detect the order of merging and persist the C++ state attached to styles. Reanimated flattens arrays passed to the `style` prop, making it impossible to distinguish which props are animated and which are provided by `Unistyles`. ### How To fix this, we just need to remove ~two~ one occurrence~s~ of array flattening. I tested it locally with `Unistyles`, and I can correctly retrieve the `style` prop as-is. `Unistyles` have to retrieve `ref` + `style` prop while [borrowing ref](https://www.unistyl.es/v3/other/babel-plugin#3-component-factory-borrowing-ref). Additionally, with this change, I can treat `Reanimated` styles as inline styles. Since inline styles are not processed by `Unistyles` algorithm, this can completely eliminate issues such as preventing animations when switching themes or changing device orientation. ## Test plan I tested it locally and I'm happy with the outcome. But I do understand that you have own test suites and regression tests, so I hope this won't break anything. --- .../__tests__/Animation.test.tsx | 4 +-- .../__tests__/props.test.tsx | 26 ++++++++++++------- .../AnimatedComponent.tsx | 10 ++++--- .../createAnimatedComponent/PropsFilter.tsx | 6 ++--- .../react-native-reanimated/src/jestUtils.ts | 7 ++--- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/react-native-reanimated/__tests__/Animation.test.tsx b/packages/react-native-reanimated/__tests__/Animation.test.tsx index 92a15d716c5..85f037a8ada 100644 --- a/packages/react-native-reanimated/__tests__/Animation.test.tsx +++ b/packages/react-native-reanimated/__tests__/Animation.test.tsx @@ -67,7 +67,7 @@ describe('Tests of animations', () => { const { getByTestId } = render(); const view = getByTestId('view'); const button = getByTestId('button'); - expect(view.props.style.width).toBe(0); + expect(view.props.style).toEqual([getDefaultStyle(), { width: 0 }]); expect(view).toHaveAnimatedStyle(style); fireEvent.press(button); jest.advanceTimersByTime(600); @@ -92,7 +92,7 @@ describe('Tests of animations', () => { const view = getByTestId('view'); const button = getByTestId('button'); - expect(view.props.style.width).toBe(0); + expect(view.props.style).toEqual([getDefaultStyle(), { width: 0 }]); expect(view).toHaveAnimatedStyle(style); fireEvent.press(button); diff --git a/packages/react-native-reanimated/__tests__/props.test.tsx b/packages/react-native-reanimated/__tests__/props.test.tsx index 35e67fdaeeb..d23d27d8054 100644 --- a/packages/react-native-reanimated/__tests__/props.test.tsx +++ b/packages/react-native-reanimated/__tests__/props.test.tsx @@ -3,12 +3,12 @@ import type { ViewStyle } from 'react-native'; import { Pressable, Text, View } from 'react-native'; import Animated, { + getAnimatedStyle, interpolate, interpolateColor, useAnimatedStyle, useSharedValue, } from '../src'; -import { getAnimatedStyle } from '../src/jestUtils'; import { processBoxShadow } from '../src/processBoxShadow'; const AnimatedPressable = Animated.createAnimatedComponent(Pressable); @@ -86,9 +86,12 @@ describe('Test of boxShadow prop', () => { const { getByTestId } = render(); const pressable = getByTestId('pressable'); - expect(pressable.props.style.boxShadow).toBe( - '0px 4px 10px 0px rgba(255, 0, 0, 1)' - ); + expect(pressable.props.style).toEqual([ + { + boxShadow: '0px 4px 10px 0px rgba(255, 0, 0, 1)', + }, + getDefaultStyle(), + ]); expect(pressable).toHaveAnimatedStyle(style); fireEvent.press(pressable); jest.advanceTimersByTime(600); @@ -112,13 +115,18 @@ describe('Test of boxShadow prop', () => { const { getByTestId } = render(); const pressable = getByTestId('pressable'); - expect(pressable.props.style.boxShadow).toBe( - '0px 4px 10px 0px rgba(255, 0, 0, 1)' - ); + expect(pressable.props.style).toEqual([ + { + boxShadow: '0px 4px 10px 0px rgba(255, 0, 0, 1)', + }, + getDefaultStyle(), + ]); + + const unprocessedStyle = getAnimatedStyle(pressable) as ViewStyle; - processBoxShadow(pressable.props.style); + processBoxShadow(unprocessedStyle); - expect(pressable.props.style.boxShadow).toEqual([ + expect(unprocessedStyle.boxShadow).toEqual([ { offsetX: 0, offsetY: 4, diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx index c0be4d03fb0..3a0005f334d 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx +++ b/packages/react-native-reanimated/src/createAnimatedComponent/AnimatedComponent.tsx @@ -460,10 +460,12 @@ export default class AnimatedComponent filteredProps.entering && !getReducedMotionFromConfig(filteredProps.entering as CustomConfig) ) { - filteredProps.style = { - ...(filteredProps.style ?? {}), - visibility: 'hidden', // Hide component until `componentDidMount` triggers - }; + filteredProps.style = Array.isArray(filteredProps.style) + ? filteredProps.style.concat([{ visibility: 'hidden' }]) + : { + ...(filteredProps.style ?? {}), + visibility: 'hidden', // Hide component until `componentDidMount` triggers + }; } const platformProps = Platform.select({ diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx index 8d915242bb3..d5d24bc386c 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx +++ b/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx @@ -1,7 +1,5 @@ 'use strict'; -import { StyleSheet } from 'react-native'; - import { initialUpdaterRun } from '../animation'; import type { StyleProps } from '../commonTypes'; import { isSharedValue } from '../isSharedValue'; @@ -53,7 +51,9 @@ export class PropsFilter implements IPropsFilter { return style; } }); - props[key] = StyleSheet.flatten(processedStyle); + // keep styles as they were passed by the user + // it will help other libs to interpret styles correctly + props[key] = processedStyle; } else if (key === 'animatedProps') { const animatedProp = inputProps.animatedProps as Partial< AnimatedComponentProps diff --git a/packages/react-native-reanimated/src/jestUtils.ts b/packages/react-native-reanimated/src/jestUtils.ts index e82ba6f91f9..b2690ce2821 100644 --- a/packages/react-native-reanimated/src/jestUtils.ts +++ b/packages/react-native-reanimated/src/jestUtils.ts @@ -62,8 +62,6 @@ const getCurrentStyle = (component: TestComponent): DefaultStyle => { ...style, }; }); - - return currentStyle; } const jestInlineStyles = component.props.jestInlineStyle as JestInlineStyle; @@ -84,7 +82,6 @@ const getCurrentStyle = (component: TestComponent): DefaultStyle => { } currentStyle = { - ...styleObject, ...currentStyle, ...jestAnimatedStyleValue, }; @@ -95,8 +92,8 @@ const getCurrentStyle = (component: TestComponent): DefaultStyle => { const inlineStyles = getStylesFromObject(jestInlineStyles); currentStyle = isEmpty(jestAnimatedStyleValue as object) - ? { ...styleObject, ...inlineStyles } - : { ...styleObject, ...jestAnimatedStyleValue }; + ? { ...inlineStyles } + : { ...jestAnimatedStyleValue }; return currentStyle; };