In React Native, how do you animate a like-button heart pop with Animated.spring, and what does useNativeDriver: true change?
answer
- a value, an animation, an Animated.View
- useAnimatedValue keeps one value
- timing, spring or decay
- the animation is sent to native up front
- useNativeDriver must be set explicitly
basics
~10 sKeep 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 sYou 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 linesimport {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
Recall the three pieces: an Animated.Value from useAnimatedValue, a timing, spring or decay animation, and an Animated component, plus what useNativeDriver does.
Explain why the native driver survives a busy JS thread, the defaults of timing and spring, and why the option must be explicit.
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.
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