Reanimated
Shared values and useAnimatedStyle run the animation on the UI thread, so it holds its frame rate while JavaScript is busy.
const sv = useSharedValue(0); const style = useAnimatedStyle(() => ({ opacity: sv.value }));Reanimated is a separate package that evaluates your animation on the UI thread instead of the JavaScript thread. Expo SDK 57 pins react-native-reanimated 4.5.1; install it with npx expo install react-native-reanimated react-native-worklets — Reanimated 4 moved worklets into the separate react-native-worklets package, which must be installed alongside it — and babel-preset-expo configures the required Babel plugin for you. Reanimated 4 needs the New Architecture, which has been the default since React Native 0.76.
Shared values
useSharedValue(0) returns a box holding a value. Read and write it with .value, or with .get() and .set(): the same box with two spellings, the method form being the one the React Compiler prefers. Writing to it does not re-render the component. Never read or write it during render — that is a side effect and it breaks reactivity.
useAnimatedStyle
useAnimatedStyle(() => ({ opacity: progress.value })) returns a style object. The parentheses are the arrow shorthand for () => { return { opacity: progress.value }; }. That callback is a worklet: it is compiled to run on the UI thread, where it re-runs whenever a shared value it reads changes. Assigning withTiming(1) (300ms, Easing.inOut(Easing.quad)) or withSpring(1) animates the value rather than jumping to it. Do not mutate a shared value inside the callback; the docs call that undefined behaviour and it can loop forever.
Why it stays smooth
Because each frame is computed natively, a slow list render or a large JSON parse on the JavaScript thread cannot stall it. Reanimated 4 also accepts CSS-like style props such as animationName and animationDuration; they resemble CSS but are evaluated by Reanimated, not a browser.
Example
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated';
type LikeButtonProps = {
label: string;
};
export default function LikeButton({ label }: LikeButtonProps) {
const scale = useSharedValue(1);
const liked = useSharedValue(0);
// The body is wrapped in parentheses so the arrow returns the object.
// Longhand: useAnimatedStyle(() => { return { ... }; })
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: 0.5 + liked.value * 0.5,
}));
return (
<View style={styles.screen}>
<Pressable
onPressIn={() => {
scale.value = withSpring(0.92, { duration: 150, dampingRatio: 1 });
}}
onPressOut={() => {
scale.value = withSpring(1);
}}
onPress={() => {
// .get and .set are the same read and write as .value
liked.set(withTiming(liked.get() === 1 ? 0 : 1, { duration: 180 }));
}}>
<Animated.View style={[styles.button, animatedStyle]}>
<Text style={styles.label}>{label}</Text>
</Animated.View>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: 'center', justifyContent: 'center' },
button: { backgroundColor: '#c2185b', borderRadius: 24, paddingHorizontal: 20, paddingVertical: 12 },
label: { color: '#ffffff', fontWeight: '600' },
});When to use it
- Keep a press animation at full frame rate while a long list is still rendering.
- Animate a progress bar's width, which the Animated native driver will not take.
- Derive rotation, opacity and offset from a single open-or-closed value.
- Hand a gesture callback something it can write to on the UI thread.
More examples
Animating a layout property, which the native driver cannot do
On screen: a grey track with a green fill that slides to its new length over 250ms whenever the fraction prop changes. This is the case the built-in Animated API handles badly - width is a layout property, so the native driver will not take it. Percentages are measured here with onLayout rather than guessed, because a percentage width inside a flex row is not the same thing as a CSS percentage of the viewport.
import { useEffect, useState } from 'react';
import { LayoutChangeEvent, StyleSheet, Text, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
type ProgressBarProps = {
fraction: number; // 0 to 1
};
export default function ProgressBar({ fraction }: ProgressBarProps) {
const [trackWidth, setTrackWidth] = useState(0);
const filled = useSharedValue(0);
const onLayout = (event: LayoutChangeEvent) => {
setTrackWidth(event.nativeEvent.layout.width);
};
// Writing in an effect, never during render.
useEffect(() => {
filled.value = withTiming(fraction * trackWidth, { duration: 250 });
}, [fraction, trackWidth, filled]);
// width is a layout property; Reanimated still runs it on the UI thread.
const fillStyle = useAnimatedStyle(() => ({ width: filled.value }));
return (
<View style={styles.wrapper}>
<View style={styles.track} onLayout={onLayout}>
<Animated.View style={[styles.fill, fillStyle]} />
</View>
<Text style={styles.caption}>{Math.round(fraction * 100)}%</Text>
</View>
);
}
const styles = StyleSheet.create({
wrapper: { padding: 16, gap: 8 },
track: { height: 8, borderRadius: 4, backgroundColor: '#eceff1', overflow: 'hidden' },
fill: { height: 8, borderRadius: 4, backgroundColor: '#2e7d32' },
caption: { fontSize: 12, color: '#546e7a' },
});One source value, several styles
On screen: tapping the header rotates the chevron a quarter turn and fades the body text in; tapping again reverses both. One shared value is the single source of truth, useDerivedValue computes a read-only value from it on the UI thread, and two animated styles consume the result. Reach for this when several properties must stay in step - keeping two shared values in sync by hand is how animations end up out of phase.
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Animated, {
Extrapolation,
interpolate,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
type DisclosureProps = {
title: string;
body: string;
};
export default function Disclosure({ title, body }: DisclosureProps) {
const open = useSharedValue(0);
// interpolate(value, inputRange, outputRange, extrapolate?)
const rotation = useDerivedValue(() =>
interpolate(open.value, [0, 1], [0, 90], Extrapolation.CLAMP),
);
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
const bodyStyle = useAnimatedStyle(() => ({ opacity: open.value }));
return (
<View style={styles.card}>
<Pressable
style={styles.header}
onPress={() => {
open.value = withTiming(open.value === 0 ? 1 : 0, { duration: 200 });
}}>
<Text style={styles.title}>{title}</Text>
<Animated.Text style={chevronStyle}>></Animated.Text>
</Pressable>
<Animated.View style={bodyStyle}>
<Text>{body}</Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
card: { margin: 16, padding: 12, borderRadius: 12, backgroundColor: '#ffffff', gap: 8 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
title: { fontSize: 16, fontWeight: '600' },
});A reusable toggle, both spellings of a write
A custom hook that owns one 0-to-1 shared value and the function that flips it, so screens import the behaviour instead of repeating it. It also puts the two spellings side by side: sv.value = x and sv.set(x) are the same write, as are sv.value and sv.get(). You will see both in other people's code and in the Reanimated docs. Reading and writing here is safe because it happens inside a callback, not during render.
import { useCallback } from 'react';
import { useSharedValue, withTiming, type SharedValue } from 'react-native-reanimated';
type ToggleProgress = {
progress: SharedValue<number>;
toggle: () => void;
};
export default function useToggleProgress(duration = 200): ToggleProgress {
const progress = useSharedValue(0);
const toggle = useCallback(() => {
// Property form
progress.value = withTiming(progress.value === 0 ? 1 : 0, { duration });
// Method form - identical, and what the React Compiler expects:
// progress.set(withTiming(progress.get() === 0 ? 1 : 0, { duration }));
}, [duration, progress]);
return { progress, toggle };
}
Discussion