Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic from value doesn't update after state change in MotiView #356

Open
2 of 3 tasks
caticodev opened this issue Aug 5, 2024 · 18 comments
Open
2 of 3 tasks

Dynamic from value doesn't update after state change in MotiView #356

caticodev opened this issue Aug 5, 2024 · 18 comments

Comments

@caticodev
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Do you want this issue prioritized?

  • Yes, I have sponsored
  • Not urgent

Current Behavior

The value used in the from property of the MotiView animation does not update dynamically after the corresponding state variable is updated. As a result, the initial value remains unchanged throughout the animation despite the state update.

Expected Behavior

The value in the from property should update to the new state value after the state change. The animation should then use this updated value for subsequent animations.

Steps To Reproduce

  1. Create a React Native component using moti.
  2. Initialize a state variable and set it to an initial value.
  3. Use the state variable in the from property of a MotiView animation.
  4. Add a handler that updates the state variable to a new value after the first animation completes.
  5. Observe that the value used in the from property does not change to the new state value after the state update.

Versions

- Moti: 0.29.0
- Reanimated: 3.10.1
- React Native: 0.74.2

Screenshots

No response

Reproduction

https://stackblitz.com/edit/nextjs-czkf4w?file=pages%2Findex.tsx

@nandorojo
Copy link
Owner

This is the entire purpose of the from prop. It’s the initial state. If you want to make reactive changes, you can pass them to them animate prop and they will update

@nandorojo
Copy link
Owner

I can see the case you want it for is for repeating animations. Perhaps you could instead try a sequence inside of animate rather than from?

Alternatively you could change the key…though I know this is unlikely to yield the behavior you want

@caticodev
Copy link
Author

@nandorojo I see. I have tried the sequence animation, but I also need to know when the animation has ended to trigger the change of from value and I haven't figured out how to listen to sequence end. The onDidAnimate is triggered by every partial animation in the sequence and all the values returned by onDidAnimate are same for every step in the sequence.

<MotiView
      animate={{
        translateY: [from, 0, from]
      }}
      transition={{
        type: 'timing',
        duration: 1000,
        repeat: 1,
        repeatReverse: true
      }}
      onDidAnimate={(_a, _b, val, other) => {
        // how to know when the sequence ended?
      }}
      style={styles.shape}
    />

@nandorojo
Copy link
Owner

you can pass objects to the sequence values, each of which can receive its own callback

@nandorojo
Copy link
Owner

(i think…)

one argument of onDidAnimate should also include attemptedSequenceValue

@caticodev
Copy link
Author

caticodev commented Aug 5, 2024

one argument of onDidAnimate should also include attemptedSequenceValue

I tried to check the attemptedSequenceValue on the combined onDidAnimate like this:

<MotiView
      animate={{
        translateY: [from, 0, from]
      }}
      transition={{
        type: 'timing',
        duration: 1000,
        repeat: 1,
        repeatReverse: true
      }}
      onDidAnimate={(prop, finished, value, events) => {
        console.log({ prop, finished, value, events })
      }}
      style={styles.shape}
    />

but onDidAnimate fires 3 times in this case and this is the log:
Screenshot 2024-08-05 at 17 13 12

since both first and third log have attempted sequence value 100, it's not possible (just from the log data themselves) to know when the sequence has ended

you can pass objects to the sequence values, each of which can receive its own callback

Thanks, didn't know that. This seems to work in the moti stackblitz template correctly and the onDidAnimate fires only once after the third step on the animation sequence is finished.

<MotiView
      animate={{
        translateY: [
          0,
          val,
          {
            value: 0,
            type: "timing",
            onDidAnimate: (finished, val, events) => {
              console.log({ finished, val, events });
              if (finished) setValue(-100);
            },
          },
        ],
      }}
      transition={{
        type: "timing",
        duration: 1000,
        repeat: 1,
        repeatReverse: true,
      }}
      style={styles.shape}
    />

However, when I'm trying the same code in expo onDidAnimate fires multiple times during the animation (it seems like it fires at the init and then after every step, so 4 times in total):

Here's the repo testing the same code in expo, doesn't seem to run in stackblitz correctly unfortunately

@nandorojo
Copy link
Owner

editing repeat animations is always a tricky thing, since the component has to retain state across renders. is it possible to set key={val} to satisfy your use case?

@caticodev
Copy link
Author

Adding key={val} on MotiView doesn't seem to make a difference. The animation works correctly when written in reanimated directly.

@nandorojo
Copy link
Owner

Got it. I do wonder if perhaps reanimated is the right candidate for this one. Unless there’s a repro that works in reanimated and not moti

@caticodev
Copy link
Author

There is a repro that works in reanimated and not in moti.

This works as expected in reanimated:

const [val, setValue] = useState(100);
const translateY = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ translateY: translateY.value }],
}));

useEffect(() => {
  translateY.value = withRepeat(
    withTiming(val, { duration: 1000 }),
    2,
    true,
    () => {
      runOnJS(setValue)(-100);
    }
  );
}, [val]);

return <Animated.View style={[styles.shape, animatedStyle]} />;

and this would be the same code in Moti (based on your suggestion to create a sequence and put onDidAnimate callback at the end of the sequence):

const [val, setValue] = useState(100);

return (
  <MotiView
    animate={{
      translateY: [
        0,
        val,
        {
          value: 0,
          type: "timing",
          onDidAnimate: () => {
            setValue(-100);
          },
        },
      ],
    }}
    transition={{
      type: "timing",
      duration: 1000,
    }}
    style={styles.shape}
  />

I believe it comes down to moti lacking more granular onAnimationEnd callbacks that are available in reanimated - in this case there's no "on repeat end" callback, that would tell me with certainty when the full animation is completed. And the "on sequence step end" callback doesn't seem to work properly in expo, since it fires multiple times instead of firing only once after the specific sequence step has ended.

I've updated the examples in the repo with both of these.

@nandorojo
Copy link
Owner

you’re correct, there’s no onRepeatEnd callback. that’s an interesting case I hadn’t come across. I wonder what the best API for this would be…

@nandorojo
Copy link
Owner

And the "on sequence step end" callback doesn't seem to work properly in expo, since it fires multiple times instead of firing only once after the specific sequence step has ended.

I assume reanimated has the same issue if you add a callback on the last item? assuming you used a sequence for the repro and not a basic withRepeat

@caticodev
Copy link
Author

I assume reanimated has the same issue if you add a callback on the last item? assuming you used a sequence for the repro and not a basic withRepeat

Even when using sequence in reanimated and putting the callback at last item of the sequence, the code still works as expected and the callback fires as expected only after the last step of the sequence is completed.

 const [val, setValue] = useState(100);
  const translateY = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));

  useEffect(() => {
    translateY.value = withSequence(
      withTiming(val, { duration: 1000 }),
      withTiming(0, { duration: 1000 }, () => {
        runOnJS(setValue)(-100);
      })
    );
  }, [val]);

  return <Animated.View style={[styles.shape, animatedStyle]} />;

So reanimated doesn't have the same issue as moti.

@nandorojo
Copy link
Owner

for a true repro you can drop the shared value and directly set the style in useAnimatedStyle. this is what moti does

@caticodev
Copy link
Author

for a true repro you can drop the shared value and directly set the style in useAnimatedStyle. this is what moti does

I'm assuming you mean like this:

const [val, setValue] = useState(100);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      {
        translateY: withSequence(
          withTiming(0, { duration: 0 }),
          withTiming(val, { duration: 1000 }),
          withTiming(0, { duration: 1000 }, () => {
            runOnJS(setValue)(-100);
          })
        ),
      },
    ],
  }));

  return <Animated.View style={[styles.shape, animatedStyle]} />;

Still works correctly in reanimated.

@nandorojo
Copy link
Owner

got it, so it sounds like identified bug is that onDidAnimate for sequence items fires multiple times, but only on native

@caticodev
Copy link
Author

correct, the bug happens only on expo
I updated the repo with the lastest repros in case you need it

@nandorojo
Copy link
Owner

I think this is only for transforms, and I think it's because it may be firing for both transform as well as the nested value. Have to look into that more.

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

No branches or pull requests

2 participants