React Native – Animations and the Native Driver

July 7, 20263 min readUpdated 8/24/2026

There are two threads. Your JavaScript runs on one of them, and the screen is drawn on the other. Every animation decision in React Native comes back to that fact.

Animated, in three parts

You create a value, you animate it, and you bind it to a style on an Animated component.

  const [enter] = useState(() => new Animated.Value(0));

An Animated.Value is a mutable number that lives outside React's state. It is created once and never replaced, which is why it goes in a lazy useState initialiser — the function runs on the first render only.

⚠️ Most React Native code writes useRef(new Animated.Value(0)).current, and it works. It also reads ref.current during render, which React 19's react-hooks/refs rule now rejects — a ref is not meant to be part of rendering, and the React Compiler is entitled to assume it is not. useState with a lazy initialiser gives the same guarantee without lying about what it is.

Start it in an effect

  useEffect(() => {
    const animation = Animated.timing(enter, {
      toValue: 1,
      duration: 180,
      useNativeDriver: true,
    });
    animation.start();

    return () => animation.stop();
  }, [enter]);

Not in the render body. Render must be side-effect free — React may render a component twice, and StrictMode does exactly that in development, so starting an animation from render starts it twice.

The cleanup matters as much. Stopping the animation on unmount is what stops it writing to a node that is no longer there, and on a screen that mounts and unmounts rows quickly that adds up.

useNativeDriver, and what it costs you

This is the flag that decides whether an animation is smooth.

With useNativeDriver: true, the whole animation is serialised and handed to the platform's own animation system before it starts. It then runs on the UI thread, untouched by JavaScript — so it stays at 60fps while your code is busy parsing a menu response or re-rendering a list.

Without it, every frame is computed in JavaScript and sent across. If the JavaScript thread is busy, the animation stutters, and it will be busy at exactly the moments animations matter: during a navigation, while data loads.

The catch: only transform and opacity can be driven natively. Not height, not backgroundColor, not width, not layout properties at all — those require a layout pass, which only the JavaScript side can trigger.

That constraint shapes how you animate. Collapsing a panel by animating its height is the obvious approach and the slow one; scaling or translating it is the fast one. When you find yourself wanting to animate a layout property, look for a transform that produces the same impression.

interpolate

One driving value can produce several effects, which keeps them in lockstep:

        {
          opacity: enter,
          transform: [
            { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) },
          ],
        },

opacity takes the 0→1 value directly. translateY maps that same value onto -8→0, so the toast slides down as it fades in. One animation, two visible effects, and they cannot drift apart because there is only one source.

interpolate handles more than two stops, and can map to strings — which is how you animate a rotation: outputRange: ['0deg', '360deg'].

Animated components

Only Animated.View, Animated.Text, Animated.Image and Animated.ScrollView can take animated values. Passing one to a plain View does nothing useful. Animated.createAnimatedComponent wraps your own component if it needs to participate.

The other animation APIs

LayoutAnimation animates the next layout change automatically — one call before a state update and every affected view eases into its new position. It is remarkably little code for adding and removing list rows, and correspondingly hard to control precisely.

Animated.spring instead of timing gives physical motion, which usually feels better for anything the user dragged.

Reanimated is the serious option, and worth knowing where it fits. It runs your animation logic itself on the UI thread — not just the interpolation — so a gesture can drive an animation with no JavaScript round trip at all. That is what you need for a draggable sheet or a swipe-to-delete row. It costs a native dependency and a mental model built on worklets, so it earns its place when gestures drive animation and not before.

Respecting the user

AccessibilityInfo.isReduceMotionEnabled() reports whether the OS setting is on. The web has prefers-reduced-motion; this is the same courtesy, and it is one if around the decorative animations.

What is next

Navigation — a stack of native screens, and a router where the folder structure is the graph.