software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
8.85k stars 1.29k forks source link

useSharedValue reinitialize on re-render #3224

Open ethanshar opened 2 years ago

ethanshar commented 2 years ago

Description

It appears useSharedValue reset the value on each render of a component, instead of setting it once with the initial value. I notice this issue https://github.com/software-mansion/react-native-reanimated/issues/1655 but wasn't 100% sure if this is something that was fixed.

Expected behavior

useSharedValue should initialize the value once with provided initial value.

Actual behavior & steps to reproduce

Run the snippet below to reproduce

Snack or minimal code example

function getInitialValue() {
  console.log('this is being called on each render')
  return [];
}

function MyComponent() {
  const myData = useSharedValue(getInitialValue())  

  useAnimatedReaction(() => myData.value,
    () => {
      console.log('myData has changed');
    });

  return null;
}

function ParentComponent() {
  const [counter, setCounter] = useState();

  return <View>
    <TouchableOpacity onPress={() => setCounter(counter + 1)}>
       <Text>{counter}</Text>
    </TouchableOpacity>
    <MyComponent/>
  </View>
}

Package versions

name version
react-native 0.66.4
react-native-reanimated 2.8.0
NodeJS 14.17.0
Xcode 13.1
Java
Gradle
expo

Affected platforms

andreialecu commented 2 years ago

I don't think this is a Reanimated issue.

This is how JavaScript works. You're actually calling getInitialValue() yourself (look at the code again).

I think you're looking for useSharedValue(() => getInitialValue()) instead. (nevermind, that doesn't seem to exist, you'd need to memoize yourself)

See here for a similar discussion about useState: https://kentcdodds.com/blog/use-state-lazy-initialization-and-function-updates#usestate-lazy-initialization

nandorojo commented 2 years ago

yeah, as @andreialecu, by calling the function, you are calling it on each render. the behavior is correct.

useState lets you pass a function and under the hood it only calls it for you once. useSharedValue doesn't have a lazy API, I believe. Maybe you can do something like this:

const value = useSharedValue(null)

if (value.value === null) {
  value.value = getValueLazily()
}

I'm not sure if this is safe for concurrent mode or not. It may be fine.

This issue should be closed though

ethanshar commented 2 years ago

I would expect useSharedValue to work similarly to useState or useRef. Creating a variable using one of these hooks preserve its value through renders.

Having to memoize the shared value sound trivial to me, why would anyone don't want to memoize it? Shouldn't it be memoized by the hook internally? Does it make sense for a shared value to reset its value on each render? to me It sounds like a bug.

just to clarify, it has nothing to do with the fact I'm initializing the shared value with getInitialValue() method. I could initialize it with a static value, like this const myData = useSharedValue(true) It would still trigger a reaction (useAnimatedReaction) on each render.

andreialecu commented 2 years ago

I could be wrong, but you need to pass a dependency array to useAnimatedReaction, otherwise it's similar to useEffect and it runs on every render regardless of using any shared values or not.

You can confirm this by changing the code to:

  useAnimatedReaction(() => 0, () => console.log('myData has changed'));

It will log that myData has changed every time. So this is not related to useSharedValue

Using the following will correctly only log it once:

  useAnimatedReaction(() => myData.value,
    () => {
      console.log('myData has changed');
    }, []); // or [myData]

I would agree that the documentation related to useAnimatedReaction's dependency array argument could be better.

Snack for easy testing: https://snack.expo.dev/KtdoeN4r2

nandorojo commented 2 years ago

useAnimationReaction‘s function receives both a new and old argument. you can compare these two in the function and only do something if it has changed.

ethanshar commented 2 years ago

useAnimationReaction‘s function receives both a new and old argument. you can compare these two in the function and only do something if it has changed.

Yes, I'm well aware and that's exactly the problem I'm facing. Because the shared value is being reset on render, I receive a reaction on this change

andreialecu commented 2 years ago

@ethanshar re-read my message just above, it explains why this happens and has nothing to do with the shared value. There's also a snack you can experiment with.

useAnimatedReaction(() => 0, () => console.log('myData has changed')); prints the same message

ikedm commented 2 years ago

@ethanshar did you find a work around for this? I'm facing the same issue with useSharedValue and useAnimatedStyle

vkukade-altir commented 8 months ago

Unfortunately, this issue still exists. Shared value gets reset on every re-render of component!

antoinerousseau commented 6 months ago

I had a similar bug (on Android only, not iOS, no idea why) where the initial opacity would stay at 1 instead of 0 even though I wrote useSharedValue(0). Replacing it with useAnimatedStyle() solved it for me.

zay1x commented 2 months ago

Explain: Each time component is re-render. sharedValue has not been updated yet. It reset to init value as you mentioned above. So, you can update it after your component done for re-render.

interface Props {
   counter: number;
    ...
};

const Child = ({counter}: Props) => {
    const sharedValue = useSharedValue(0);

    useEffect(() => {
        const timeoutId = setTimeOut(() => {    // <= Here is solution for me
            sharedValue.value = counter;
        }, 0);
        return () => clearTimeout(timeoutId);
    }, [counter]);

    return <View>
        <Text>{counter}</Text>
    </View>
};

export default memo(Child);