skip to content

In React Native Animated, how do sequence, parallel, stagger and loop differ, and what happens when a child animation is interrupted?

level: middleimportance: should knowfreq 40%

answer

  1. one after another, or together
  2. stagger is delayed parallel starts
  3. parallel stops together by default
  4. loop repeats forever by default
  5. only a single native animation loops natively

basics

~20 s

sequence runs animations one after another, parallel starts them together, stagger starts them in parallel with increasing delays, and loop repeats one animation. An interrupted child stops a sequence, and stops all of a parallel unless stopTogether is false.

solid answer

~50 s

`Animated.sequence([a, b])` starts `b` only when `a` finishes; if `a` ends with `finished: false`, for example because another animation took over its value, the sequence stops and reports `finished: false`. `Animated.parallel([a, b])` starts all at once, and by default (`stopTogether: true`) one child stopping stops the rest. `Animated.stagger(ms, [a, b, c])` is a parallel in which each child waits `ms` more than the previous one. `Animated.loop(anim)` repeats it; `iterations` defaults to -1, meaning forever, and `resetBeforeIteration` defaults to true. The native-driver subtlety: a loop runs natively only when it wraps a single native-driven animation. Looping a `sequence`, `parallel` or `stagger` is restarted from JavaScript between iterations, even if every child uses the native driver, and `Animated.delay` is always JS-driven. For a like pop that means: sequence the scale-up and spring-back, stagger the burst particles, and loop only a simple native pulse.

code

tsx · 37 lines
tsx
import {useEffect, useState} from 'react';
import {Animated, useAnimatedValue} from 'react-native';

export function useLikeBurst(particleCount: number) {
  const scale = useAnimatedValue(1);
  const [particles] = useState(() =>
    Array.from({length: particleCount}, () => new Animated.Value(0)),
  );

  const play = () => {
    scale.setValue(1);
    particles.forEach(p => p.setValue(0));
    const pop = Animated.sequence([
      Animated.timing(scale, {toValue: 1.3, duration: 120, useNativeDriver: true}),
      Animated.spring(scale, {toValue: 1, friction: 4, useNativeDriver: true}),
    ]);
    const burst = Animated.stagger(
      40,
      particles.map(p => Animated.timing(p, {toValue: 1, duration: 300, useNativeDriver: true})),
    );
    Animated.parallel([pop, burst], {stopTogether: false}).start();
  };

  return {scale, particles, play};
}

export function usePulse() {
  const pulse = useAnimatedValue(0);
  useEffect(() => {
    const loop = Animated.loop(
      Animated.timing(pulse, {toValue: 1, duration: 900, useNativeDriver: true}),
    );
    loop.start();
    return () => loop.stop();
  }, [pulse]);
  return pulse.interpolate({inputRange: [0, 0.5, 1], outputRange: [1, 1.08, 1]});
}

go deeper

for a junior

Recall the four composers: one after another, all together, together with increasing delays, and repeat.

for a middle

Explain the defaults, stopTogether true and iterations -1, and how finished: false propagates through a sequence and a parallel.

for a senior

Design composite animations that survive interruption and stay native where it matters, knowing that loops over composites return to JavaScript.

for a principal

Set guidance for reusable motion building blocks so product teams compose them without reintroducing JS-driven hitches or stuck states.

## The four composers `Animated` provides functions that combine animations into a **composite animation** with the same `start`, `stop` and `reset` methods as a single one. | Composer | Behaviour | Default options | |---|---|---| | `Animated.sequence([...])` | runs children one after another | none | | `Animated.parallel([...], config)` | starts all children at once | `stopTogether: true` | | `Animated.stagger(ms, [...])` | parallel, child *i* delayed by `ms × i` | built on `parallel` and `sequence` | | `Animated.loop(anim, config)` | repeats one animation | `iterations: -1` (forever), `resetBeforeIteration: true` | | `Animated.delay(ms)` | waits, as a child in a sequence | JS-driven timing | ## A like-button burst built from them - **Pop:** `sequence([timing(scale → 1.3), spring(scale → 1)])` grows the heart and springs it back. - **Particles:** `stagger(40, particles.map(p => timing(p → 1)))` fires six little dots one after another. - **Together:** `parallel([pop, particles])` runs both at once. - **Idle pulse:** on a "liked" badge, `loop(timing(pulse → 1))`, so it breathes while visible. ## What interruption does Every animation ends with a result `{finished}`. It is `false` when the animation was **stopped early**: by calling `stop()`, by starting another animation on the same value, or by `setValue`. 1. **sequence:** when a child ends with `finished: false`, the sequence does not start the next child; it stops and calls its own callback with `finished: false`. A heart interrupted after the scale-up child can stay at 1.3 unless something else drives it back. 2. **parallel:** with the default `stopTogether: true`, a child that ends unfinished stops all the others. Set `stopTogether: false` when children are independent. 3. **stagger:** inherits the parallel behaviour. 4. **loop:** an iteration that ends unfinished ends the loop; `stop()` on the loop ends it too. ## Loops and the native driver A subtle point interviewers probe: - `Animated.loop` checks whether its child **is using the native driver**. A single `timing`, `spring` or `decay` with `useNativeDriver: true` loops **entirely natively**, with the iteration count sent to native. - `sequence`, `parallel` and `stagger` report that they are **not** using the native driver as a whole, even if every child is native-driven. A loop over them therefore restarts each iteration **from JavaScript**, and a busy JS thread shows up as a hitch between iterations. - `Animated.delay` is implemented as a JS-driven timing, so a sequence containing a delay always involves JavaScript for that step. For a smooth infinite pulse, loop one native-driven `timing` and use `interpolate` for the up-and-down shape instead of a sequence of two timings. ## Common mistakes - Expecting a sequence to continue after an interrupted child. - Leaving `stopTogether` at its default when one child is meant to finish independently. - Looping a sequence and blaming the native driver for hitches between iterations. - Forgetting to `stop()` a forever loop when the component unmounts. ## Choosing between them 1. **Does one step depend on another finishing?** Use `sequence`. 2. **Do independent effects start together?** Use `parallel`, and decide deliberately whether they should `stopTogether`. 3. **Should similar items start one after another but overlap?** Use `stagger` with a small interval, such as 30 to 60 ms for particles. 4. **Should something repeat while visible?** Use `loop`, preferably around a single native animation, and stop it on unmount. ## Reading results - Every composite's `start` callback receives `{finished}` just like a single animation. - For `parallel`, the callback fires once every child has ended and receives the result of the child that ended **last**; with the default `stopTogether`, an interruption stops the others, so that result is normally `finished: false`, but with `stopTogether: false` an early interruption can be hidden behind a later child that finished. - For `loop`, the callback runs when the loop ends, by reaching `iterations`, by `stop()`, or by an interrupted iteration. - Calling `start` again on a sequence that finished starts it from the beginning; the implementation resets its internal position when it completes.

  • Why does Animated.loop(Animated.sequence([...])) hitch when the JS thread is busy, even with useNativeDriver: true on every child?
    A loop runs natively only if its child reports that it uses the native driver, and a sequence always reports that it does not. So the loop restarts each iteration from JavaScript, and each child in the sequence is started from JS when the previous one ends. The children animate natively, but the handovers wait for the JS thread.
  • In React Native Animated, what does resetBeforeIteration do in Animated.loop?
    With the default `true`, the looped animation is reset to its starting value before each iteration, so a timing from 0 to 1 jumps back to 0 and runs again. With `false`, each iteration starts from wherever the previous one ended, which only makes sense for animations whose target changes or that are relative.

saying these in an interview costs you the question

  • Animated.sequence continues with the next child after one is interrupted
  • Animated.parallel lets the other children finish when one is stopped, by default
  • Animated.loop runs three times unless iterations is set
  • A loop over a sequence of native animations runs entirely on the UI thread
  • Animated.stagger waits for each animation to finish before starting the next