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.