An Animated.Value is a number that an animation drives and a style reads. It is not React 7,897 state: animating it re-renders nothing. Create one with useAnimatedValue(0) (shorthand for useRef(new Animated.Value(0)).current) and pass it into an Animated.View's style. Animated.ValueXY pairs x and y, with getTranslateTransform() and getLayout() to turn the pair into a style.
const opacity = useAnimatedValue(0); // one number, kept across renders
const pos = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current; // two numbers
useEffect(() => {
let frames = 0;
const id = opacity.addListener(() => { frames += 1; });
pos.setOffset({ x: 20, y: 0 }); // added to whatever the animation sets
Animated.parallel([
Animated.timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true }),
Animated.timing(pos, { toValue: { x: 100, y: 40 }, duration: 400,
useNativeDriver: true }),
]).start(({ finished }) => {
log('finished:', finished, '| opacity updates seen by JS:', frames);
pos.flattenOffset(); // offset merged into the value
pos.stopAnimation((xy) => log('pos after flattenOffset:', JSON.stringify(xy)));
opacity.removeListener(id);
});log(), from src/anim/log.ts, prefixes the milliseconds since the demo started:
LOG +837ms finished: true | opacity updates seen by JS: 5
LOG +903ms pos after flattenOffset: {"x":120,"y":40}setValue() jumps without animating; setOffset() adds a base the animation ignores, which lets a drag continue from where the last one ended, and flattenOffset() folds it in (20 + 100 = 120); stopAnimation(cb) stops and reports the current value. A listener hears only what the native side reports: 5 updates for a 400 ms animation that took 837 ms on this busy emulator, so never drive logic from a native animation's listener.