Skip to content

[v3, Paper] Initial animated style updater style is applied on re-render #7375

@MatiPl01

Description

@MatiPl01

Description

The issue was originally reported by @efstathiosntonas on the react-native-sortables repository (here). This regression was introduced after removing style flattening in this PR and then cherry-picked to 3.17.2 in this PR.

Basically, the issue can be seen if the animated view is passed more than one animated style (the additional style can be even empty). When a view with such styles re-render, the style from the initial style updater run is applied to the view instead of the last returned style. This is best visible if the useAnimatedStyle hook is used to create a discrete style updated based a condition (e.g. a Shared Value is a flag indicating whether to return one or the other style) and doesn't contain animations.

The issue can be fixed by just adding the style flattening in this line as follows:

props[key] = StyleSheet.flatten(processedStyle);

Issue recording

Observe the logs. You can see, that when re-render happens, the width of the box changes, even though the value returned within the useAnimatedStyle callback doesn't change.

incorrect.mp4

Expected behavior

After bringing back the style flattening

valid.mp4

Steps to reproduce

  1. Copy code from the expo snack or from below
  2. Paste the code to the react-native app with reanimated 3.17.2 or 3.17.3
  3. Observe that the width of the view is set back to 100 after re-render
Source code
import { useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
  LinearTransition,
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

export default function PlaygroundExample() {
  const [_, setCount] = useState(0);

  const animatedWidth = useSharedValue(0);

  const animatedWidthStyle = useAnimatedStyle(() => {
    if (animatedWidth.value === 0) {
      console.log('predefined width');
      return { width: 100 };
    }

    console.log('animatedWidth.value', animatedWidth.value);
    return {
      width: animatedWidth.value,
    };
  });

  const emptyAnimatedStyle = useAnimatedStyle(() => ({}));

  useEffect(() => {
    animatedWidth.value = 10;

    setTimeout(() => {
      console.log('re-render');
      setCount((prev) => prev + 1);
    }, 1000);

    setTimeout(() => {
      animatedWidth.value = 20;
    }, 4000);
  }, [animatedWidth]);

  return (
    <Animated.View
      layout={LinearTransition}
      style={[emptyAnimatedStyle, animatedWidthStyle]}>
      <View style={styles.box} />
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  box: {
    backgroundColor: 'red',
    height: 100,
  },
});

Snack or a link to a repository

https://snack.expo.dev/vZOtho0RKCBlZNXu4uI1q

Reanimated version

3.17.2

React Native version

0.79.0

Platforms

Android, iOS

JavaScript runtime

None

Workflow

None

Architecture

Paper (Old Architecture)

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

Metadata

Metadata

Assignees

Labels

Maintainer issueIssue created by a maintainerRepro providedA reproduction with a snippet of code, snack or repo is provided

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions