Skip to content

Commit

Permalink
Make animatedStyle keep reference through renders (#5333)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary
Closes #1767 

Some of our hooks (`useSharedValue`, `useDerivedValue`) return immutable
refs.
```javascript
export function useSharedValue<Value>(
  init: Value,
  oneWayReadsOnly = false
): SharedValue<Value> {
  const ref = useRef<SharedValue<Value>>(makeMutable(init, oneWayReadsOnly));
  
  // code

  return ref.current;
}
```

However `useAnimatedStyle` return an object. Therefore animated style is
getting a new reference each render. This issue was noticed here:
#1767


## Test plan

<details><summary>code</summary>

```javascript
/* eslint-disable no-inline-styles/no-inline-styles */
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useDerivedValue,
  withSpring,
} from 'react-native-reanimated';
import { Button, View } from 'react-native';
import React, { useEffect, useState } from 'react';

export default function AnimatedStyleUpdateExample() {
  const [, setCounter] = useState(0);

  const width = useSharedValue(10);

  const derivedValue = useDerivedValue(() => {
    return width.value + 10;
  });

  const style = useAnimatedStyle(() => {
    return {
      width: width.value,
    };
  });

  useEffect(() => {
    setInterval(() => {
      setCounter((counterVal) => counterVal + 1);
    }, 2000);
  }, []);

  console.log('component re-render');

  useEffect(() => {
    console.log('width changed identity');
  }, [width]);

  useEffect(() => {
    console.log('derivedValue changed identity');
  }, [derivedValue]);

  useEffect(() => {
    console.log('style changed identity');
  }, [style]);

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Animated.View
        style={[style, { height: 80, backgroundColor: 'black', margin: 30 }]}
      />
      <Button
        title="TOGGLE"
        onPress={() => {
          width.value = withSpring(Math.random() * 100);
        }}
      />
    </View>
  );
}
```
<details> 

I've tested that animations still work as desired:
<summary><details>Recording</details>



https://github.com/software-mansion/react-native-reanimated/assets/56199675/d2180c2d-b5a6-4736-ad94-1d245b3a5407



</summary>

<!-- Provide a minimal but complete code snippet that can be used to
test out this change along with instructions how to run it and a
description of the expected behavior. -->

---------

Co-authored-by: Aleksandra Cynk <[email protected]>
Co-authored-by: Aleksandra Cynk <[email protected]>
Co-authored-by: Tomasz Żelawski <[email protected]>
  • Loading branch information
4 people committed Feb 19, 2024
1 parent c955e97 commit 5c61916
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 23 deletions.
22 changes: 3 additions & 19 deletions src/createAnimatedComponent/createAnimatedComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,6 @@ function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
return styles.filter((style) => style?.viewDescriptors);
}

function isSameAnimatedStyle(
style1?: StyleProps,
style2?: StyleProps
): boolean {
// We cannot use equality check to compare useAnimatedStyle outputs directly.
// Instead, we can compare its viewsRefs.
return style1?.viewsRef === style2?.viewsRef;
}

const isSameAnimatedProps = isSameAnimatedStyle;

type Options<P> = {
setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
};
Expand Down Expand Up @@ -401,14 +390,12 @@ export function createAnimatedComponent(
const hasOneSameStyle =
styles.length === 1 &&
prevStyles.length === 1 &&
isSameAnimatedStyle(styles[0], prevStyles[0]);
styles[0] === prevStyles[0];

if (!hasOneSameStyle) {
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = styles.some((style) =>
isSameAnimatedStyle(style, prevStyle)
);
const isPresent = styles.some((style) => style === prevStyle);
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
Expand Down Expand Up @@ -438,10 +425,7 @@ export function createAnimatedComponent(
});

// detach old animatedProps
if (
prevAnimatedProps &&
!isSameAnimatedProps(prevAnimatedProps, this.props.animatedProps)
) {
if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
}

Expand Down
14 changes: 10 additions & 4 deletions src/reanimated2/hook/useAnimatedStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,15 @@ For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/doc

checkSharedValueUsage(initial.value);

if (isJest()) {
return { viewDescriptors, initial, viewsRef, jestAnimatedStyle };
} else {
return { viewDescriptors, initial, viewsRef };
const animatedStyleHandle = useRef<
AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> | null
>(null);

if (!animatedStyleHandle.current) {
animatedStyleHandle.current = isJest()
? { viewDescriptors, initial, viewsRef, jestAnimatedStyle }
: { initial, viewsRef, viewDescriptors };
}

return animatedStyleHandle.current;
}

0 comments on commit 5c61916

Please sign in to comment.