skip to content

In React Native, how do you animate a like-button heart pop with Animated.spring, and what does useNativeDriver: true change?

level: juniorimportance: must knowfreq 75%

answer

  1. a value, an animation, an Animated.View
  2. useAnimatedValue keeps one value
  3. timing, spring or decay
  4. the animation is sent to native up front
  5. useNativeDriver must be set explicitly

basics

~10 s

Keep an Animated.Value from useAnimatedValue, start Animated.spring on it, and bind it to an Animated.View's transform scale. useNativeDriver: true hands the animation to native before it starts, so the UI thread computes every frame.

solid answer

~40 s

You create a value with `useAnimatedValue(1)`, which keeps one `Animated.Value` across renders, and bind it to `<Animated.View style={{transform: [{scale}]}}>`. On a like, set it to a smaller scale and call `Animated.spring(scale, {toValue: 1, friction: 3, useNativeDriver: true}).start()` so the heart springs back with an overshoot. `Animated.timing` instead eases over a fixed `duration` (default 500 ms, ease-in-out), and `Animated.decay` slows an initial `velocity` to rest. `useNativeDriver: true` serializes the animation and hands it to native code before it starts; the UI thread then computes every frame, so a busy JavaScript thread, such as one rendering the feed, cannot make the pop stutter. The option is required: leaving it out logs a warning, fails TypeScript's check, and falls back to the JS driver. The native driver only accepts non-layout styles like `transform` and `opacity`.

code

tsx · 30 lines
tsx
import {Animated, Pressable, StyleSheet, useAnimatedValue} from 'react-native';
import {useState} from 'react';

export function LikeHeart() {
  const [liked, setLiked] = useState(false);
  const scale = useAnimatedValue(1);

  const onPress = () => {
    setLiked(prev => !prev);
    scale.setValue(0.6);
    Animated.spring(scale, {
      toValue: 1,
      friction: 3,
      tension: 120,
      useNativeDriver: true,
    }).start();
  };

  return (
    <Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel="Like">
      <Animated.Text style={[styles.heart, {transform: [{scale}]}]}>
        {liked ? '\u2665' : '\u2661'}
      </Animated.Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  heart: {fontSize: 32, color: '#e0245e'},
});

go deeper

for a junior

Recall the three pieces: an Animated.Value from useAnimatedValue, a timing, spring or decay animation, and an Animated component, plus what useNativeDriver does.

for a middle

Explain why the native driver survives a busy JS thread, the defaults of timing and spring, and why the option must be explicit.

for a senior

Show judgement about when native driving is possible, which styles force the JS driver, and how an animation that must stay smooth during list rendering is designed.

for a principal

Set a team default for animation work, Animated with the native driver versus a worklet-based library, and the review rules that keep janky JS-driven animations out.

## The three pieces of an Animated animation React Native's `Animated` API is built from three parts: 1. **A value.** `Animated.Value` holds a number that changes over time; `Animated.ValueXY` holds an `{x, y}` pair for two-dimensional motion. In a function component, `useAnimatedValue(initial)` (or `useAnimatedValueXY`) creates it once and returns the same instance on every render; `new Animated.Value()` inside the component body would create a fresh one each render. 2. **An animation.** A driver such as `Animated.timing`, `Animated.spring` or `Animated.decay` moves the value toward a target. Calling `.start()` runs it, optionally with a callback receiving `{finished}`. 3. **An animated component.** `Animated.View`, `Animated.Text`, `Animated.Image` and friends accept animated values inside `style`, for example `transform: [{scale}]`. ## Choosing a driver | Driver | What it models | Key options and defaults | |---|---|---| | `Animated.timing` | a curve over fixed time | `duration` 500 ms, `easing` ease-in-out, `delay` 0 | | `Animated.spring` | a physical spring that can overshoot | `friction` 7 and `tension` 40 by default, or `bounciness`/`speed`, or `stiffness`/`damping`/`mass`; only one family at a time | | `Animated.decay` | an initial velocity slowing to rest | `velocity` required, `deceleration` 0.998 | For a like-button heart, `spring` gives the playful overshoot: start the scale below 1 and spring to 1 with a low `friction`. Mixing families, for example `friction` with `stiffness`, fails an invariant. ## What useNativeDriver: true changes Without the native driver, JavaScript computes each frame and pushes the new value to the view. With it: - The animation is **serializable**, so React Native sends its full description to native code **before** starting it. - Native code on the **UI thread** computes every frame and applies it to the view. - Once started, the **JavaScript thread can be blocked** without affecting the animation, which matters when a like happens while the feed is rendering new posts. Three rules come with it: - **Set it explicitly.** The option is required; if it is missing, `Animated` logs a warning, TypeScript reports an error, and the JS driver is used. - **Non-layout styles only** by default: `transform`, `opacity`, colours and similar properties work; `width`, `height`, margins and positions do not. - **One driver per value.** Once a value has run a native-driven animation, running a JS-driven animation on it throws. ## Building the heart pop - Keep a `scale` value at `1`. - On press, `scale.setValue(0.6)` and start `Animated.spring(scale, {toValue: 1, friction: 3, tension: 120, useNativeDriver: true})`. - The low friction makes the spring overshoot past 1 and settle, which reads as a pop. - Toggle the liked state in React as usual; the animation is decoration, not the source of truth. ## Common mistakes - Creating `new Animated.Value(1)` in the component body, so every render resets the heart. - Omitting `useNativeDriver`, then wondering why the pop stutters while the list scrolls. - Trying to pop the heart by animating `width` and `height` with the native driver instead of `transform: [{scale}]`. - Using a plain `View` instead of `Animated.View`, so the animated value is never applied. ## Shaping the motion - **Easing for `timing`.** The default is `Easing.inOut(Easing.ease)`. `Easing.out(Easing.back())` overshoots slightly at the end, `Easing.bezier(x1, y1, x2, y2)` matches a design tool's curve, and `Easing.linear` suits progress bars. - **Delay.** `delay` in the config waits before the animation starts, useful for chaining small effects without a sequence. - **Spring feel.** Lower `friction` (or `damping`) gives more bounce; higher `tension` (or `stiffness`) makes it faster. Pick one parameter family and tune only that. - **Starting velocity.** Both `spring` and `decay` accept a `velocity`, so an animation that follows a gesture can continue its speed instead of starting from rest. - **Two dimensions.** `Animated.ValueXY` animates `{x, y}` together, and `getTranslateTransform()` turns it into a `translateX`/`translateY` transform. ## What the start callback tells you `start(({finished}) => ...)` runs when the animation ends. `finished` is `true` for a normal end and `false` when it was stopped or replaced by another animation on the same value. For a like button that users tap repeatedly, the `false` case is common, so side effects in the callback need that check.

  • In React Native Animated, when would you pick Animated.decay over timing or spring?
    When the motion starts from a velocity rather than aiming at a target, such as flinging a card or a sticker after a gesture. `decay` takes the initial `velocity` and slows it with `deceleration` (0.998 by default) until it stops, so the end position depends on how hard it was thrown. `timing` and `spring` both need a `toValue`.
  • Why use useAnimatedValue instead of new Animated.Value() in a function component?
    `new Animated.Value(1)` in the component body creates a new value on every render, so any running animation is lost and the view jumps back to the initial value. `useAnimatedValue(1)` creates the value once and returns the same instance on later renders; it is the hook form of keeping the value in a ref.

Handing a musician the full sheet music before the concert instead of whispering each note: once the score is on stage, the performance continues even if the conductor gets distracted.

saying these in an interview costs you the question

  • useNativeDriver defaults to true, so you can leave it out
  • The native driver sends a message to native on every frame
  • Animated.spring and Animated.timing both require a duration
  • A plain View will apply an Animated.Value placed in its style
  • The native driver can animate any style property, including width