hardExtra#99

Reanimated shared value animation

Prompt

Use react-native-reanimated to create a pulsing circle animation. The circle should scale between 1x and 1.5x continuously — and the animation must be cancelled when the component unmounts.

Solution

export default function PulsingCircle() {
  const scale = useSharedValue(1)

  useEffect(() => {
    scale.value = withRepeat(
      withTiming(1.5, { duration: 1000 }),
      -1,   // repeat forever
      true, // reverse: 1 → 1.5 → 1
    )
    return () => cancelAnimation(scale)
  }, [])

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

  return (
    <Animated.View
      style={[
        { width: 100, height: 100, borderRadius: 50, backgroundColor: 'blue' },
        animatedStyle,
      ]}
    />
  )
}
Mentor's take

The point of Reanimated here is where the work runs: the shared value and the useAnimatedStyle worklet live on the UI thread, so the pulse stays at frame rate even when the JS thread is busy — a setState-driven animation would push sixty re-renders a second through React and stutter under any real load.

Composition is the mechanic to narrate: withTiming(1.5, { duration: 1000 }) describes one leg, withRepeat(…, -1, true) wraps it — -1 means forever, true means reverse each iteration, which is what makes it a smooth 1 → 1.5 → 1 pulse instead of snapping back to 1 every cycle.

Two senior details. First, cancelAnimation in the effect cleanup: an infinite animation nobody cancels keeps ticking after unmount — the same slow-leak family as an uncancelled interval, and the kind of thing that only shows up as battery drain and memory growth in production. Second, the property choice: transform: scale is composited without recomputing layout; animating width/height to fake the same pulse forces layout every frame.

Red flag: reading or writing scale.value in the render body. Renders don't subscribe to shared values — the binding must live inside the useAnimatedStyle worklet, and mutation belongs in effects or gesture callbacks.

Say it: "I animate transform scale on the UI thread with withRepeat and reverse, and I cancel it in the effect cleanup — an uncancelled infinite animation is a leak that outlives the component."