Instale Reanimated e Gesture Handler com o resolvedor de versões do Expo - não fixe as versões manualmente.
npx expo install react-native-reanimated react-native-gesture-handler
Confirme o pin do SDK 57 antes de adicionar o código de animação:
{
"dependencies" : {
"expo" : "~57.0.4" ,
"react" : "19.2.3" ,
"react-native" : "0.86.0" ,
"react-native-reanimated" : "~4.0.0" ,
"react-native-gesture-handler" : "~2.28.0"
}
}
Envolva seu layout raiz com GestureHandlerRootView (necessário para gestos; inofensivo para apps apenas de animação):
// app/_layout.tsx
import { Stack } from "expo-router" ;
import { GestureHandlerRootView } from "react-native-gesture-handler" ;
export default function RootLayout () {
return (
< GestureHandlerRootView style = {{ flex: 1 }}>
< Stack />
</ GestureHandlerRootView >
);
}
Ferramentas: Estes exemplos visam o Expo SDK 57 (expo ~57.0.4), React Native 0.86.0 , React 19.2.3 e Reanimated 4 .
O plugin Babel do Reanimated reescreve worklets em tempo de compilação. Ele deve ser o último plugin no array.
// babel.config.js
module . exports = function ( api ) {
api. cache ( true );
return {
presets: [ "babel-preset-expo" ],
plugins: [
// outros plugins primeiro …
"react-native-reanimated/plugin" , // ← sempre por último
],
};
};
Se o plugin estiver ausente ou não for o último, os worklets lançarão erros em tempo de execução e as animações voltarão silenciosamente para a JS thread.
Após alterar babel.config.js, reinicie o Metro com o cache limpo: npx expo start --clear.
O template default@sdk-57 já vem com esta configuração - verifique antes de copiar trechos de tutoriais mais antigos.
Relacionado: react-native-reanimated 4 - valores compartilhados e animações na UI thread | Worklets & Bundle Mode - o que o plugin realmente faz
A API Animated integrada serve para fades únicos que não interagem com gestos.
import { useEffect, useRef } from "react" ;
import { Animated, StyleSheet, View } from "react-native" ;
export function FadeInBanner () {
const opacity = useRef ( new Animated. Value ( 0 )).current;
useEffect (() => {
Animated. timing (opacity, {
toValue: 1 ,
duration: 300 ,
useNativeDriver: true ,
}). start ();
}, [opacity]);
return (
< Animated.View style = {[styles.banner, { opacity }]}>
< View />
</ Animated.View >
);
}
const styles = StyleSheet. create ({
banner: { padding: 16 , backgroundColor: "#e8f4ff" },
});
useNativeDriver: true descarrega a opacidade e a transformação para o driver nativo - os frames não esperam por re-renderizações do React.
Animated.Value vive em um ref - recriá-lo a cada renderização reseta a animação.
Bom para toasts, esqueletos de carregamento e fades simples de montagem/desmontagem sem acoplamento de gestos.
Relacionado: Layout Animations - presets de entrada/saída quando itens montam e desmontam em listas
Combine translate e scale para ênfase leve - ainda no driver nativo.
import { useEffect, useRef } from "react" ;
import { Animated, Pressable, Text } from "react-native" ;
export function PopInButton ({ onPress } : { onPress : () => void }) {
const scale = useRef ( new Animated. Value ( 0.9 )).current;
const translateY = useRef ( new Animated. Value ( 12 )).current;
useEffect (() => {
Animated. parallel ([
Animated. spring (scale, {
toValue: 1 ,
friction: 6 ,
useNativeDriver: true ,
}),
Animated. timing (translateY, {
toValue: 0 ,
duration: 250 ,
useNativeDriver: true ,
}),
]). start ();
}, [scale, translateY]);
return (
< Pressable onPress = {onPress}>
< Animated.View
style = {{
transform: [{ scale }, { translateY }],
}}
>
< Text >Continue</ Text >
</ Animated.View >
</ Pressable >
);
}
Animated.parallel executa ambas as trilhas simultaneamente - a ordem não implica sequenciamento.
transform (translate, scale, rotate) suporta useNativeDriver: true; propriedades de layout não suportam.
Animated.spring oferece movimento orgânico sem ajustar manualmente curvas de easing para CTAs simples.
Relacionado: react-native-reanimated 4 - withSpring na UI thread para springs vinculadas a gestos
Encadeie e repita animações para indicadores de carregamento e badges pulsantes.
import { useEffect, useRef } from "react" ;
import { Animated, StyleSheet, View } from "react-native" ;
export function PulsingDot () {
const opacity = useRef ( new Animated. Value ( 0.4 )).current;
useEffect (() => {
const pulse = Animated. loop (
Animated. sequence ([
Animated. timing (opacity, {
toValue: 1 ,
duration: 600 ,
useNativeDriver: true ,
}),
Animated. timing (opacity, {
toValue: 0.4 ,
duration: 600 ,
useNativeDriver: true ,
}),
])
);
pulse. start ();
return () => pulse. stop ();
}, [opacity]);
return < Animated.View style = {[styles.dot, { opacity }]} />;
}
const styles = StyleSheet. create ({
dot: { width: 10 , height: 10 , borderRadius: 5 , backgroundColor: "#3b82f6" },
});
Sempre pare loops na limpeza do effect - loops vazados continuam disparando após a desmontagem e desperdiçam bateria.
Animated.sequence executa passos um após o outro; Animated.stagger desloca filhos paralelos.
Para esqueletos de carregamento complexos vinculados ao layout da lista, prefira animações de layout do Reanimated.
Relacionado: Melhores Práticas de Animação - quando indicadores pulsantes prejudicam o desempenho de scroll de listas
Vincule a posição de scroll diretamente a valores animados sem setState por frame.
import { useRef } from "react" ;
import { Animated, ScrollView, StyleSheet, View } from "react-native" ;
const HEADER_HEIGHT = 56 ;
export function CollapsingHeaderScroll () {
const scrollY = useRef ( new Animated. Value ( 0 )).current;
const headerOpacity = scrollY. interpolate ({
inputRange: [ 0 , HEADER_HEIGHT ],
outputRange: [ 1 , 0 ],
extrapolate: "clamp" ,
});
return (
< View style = {styles.screen}>
< Animated.View style = {[styles.header, { opacity: headerOpacity }]}>
< View />
</ Animated.View >
< Animated.ScrollView
scrollEventThrottle = { 16 }
onScroll = {Animated. event (
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
)}
>
{ /* conteúdo longo */ }
</ Animated.ScrollView >
</ View >
);
}
const styles = StyleSheet. create ({
screen: { flex: 1 },
header: { height: HEADER_HEIGHT , backgroundColor: "#111" },
});
scrollEventThrottle={16} visa eventos de scroll de ~60fps - o throttle padrão de 400ms parece lento para parallax.
interpolate mapeia o intervalo de scroll para opacidade - sem renderização do React por frame.
Para parallax vinculado a gestos além do scroll (pull-to-refresh, bottom sheets), passe para Reanimated + Gesture Handler.
Relacionado: react-native-gesture-handler - gestos de pan que compartilham valores com Reanimated
Propriedades que afetam o layout permanecem na JS thread com a API Animated clássica.
Família de propriedades useNativeDriver: trueAbordagem melhor opacity, transform✅ Suportado Animated ou Reanimatedwidth, height, top, left❌ Não suportado Animações de layout Reanimated ou Layout backgroundColor❌ Não suportado useAnimatedStyle do Reanimated + interpolateColorMudanças de Flex ❌ Não suportado Layout do Reanimated ou estrutura condicional
// ❌ Isso avisa e executa na JS thread - jank durante o scroll
Animated. timing (width, { toValue: 200 , useNativeDriver: true });
// ✅ Reanimated 4 - animação ciente de layout na UI thread
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated" ;
const progress = useSharedValue ( 0 );
const style = useAnimatedStyle (() => ({
width: withTiming (progress.value * 200 ),
}));
O limite do driver nativo é a principal razão pela qual as equipes adotam o Reanimated para movimento em produção.
Animar o layout na JS thread durante o scroll de listas é uma fonte comum de frames perdidos.
Relacionado: Layout Animations - Layout, FadeIn, e reordenação de listas sem tweens de largura manuais
Cenário API Animated Reanimated 4 Fade único ao montar ✅ Exagero Escala de clique de botão ✅ Ambos funcionam Arrastar seguindo o dedo ❌ ✅ Gesture.Pan + valores compartilhados Linha para deslizar e excluir ❌ ✅ Herói de elemento compartilhado ❌ ✅ sharedTransitionTag Movimento de inserção/exclusão de lista ❌ ✅ entering/exiting + Layout 60fps durante scroll rápido Arriscado na JS thread ✅ Worklets na UI thread
Regra geral: Se a animação segue um dedo ou executa enquanto uma FlatList está rolando, use Reanimated 4. Se for uma transição curta e "dispare e esqueça" em uma tela estática, Animated é aceitável.
Relacionado: react-native-reanimated 4 - cookbook completo para valores compartilhados | Resolução de Conflitos de Gestos - scroll vs linhas de deslizar
Valores compartilhados mutam fora do caminho de renderização do React - a base do Reanimated 4.
import { useEffect } from "react" ;
import { StyleSheet } from "react-native" ;
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated" ;
export function ReanimatedFadeIn ({ children } : { children : React . ReactNode }) {
const opacity = useSharedValue ( 0 );
useEffect (() => {
opacity.value = withTiming ( 1 , { duration: 300 });
}, [opacity]);
const style = useAnimatedStyle (() => ({
opacity: opacity.value,
}));
return < Animated.View style = {[styles.card, style]}>{children}</ Animated.View >;
}
const styles = StyleSheet. create ({
card: { padding: 16 , borderRadius: 12 , backgroundColor: "#fff" },
});
useSharedValue mantém o estado da animação na UI thread - atualizar .value não re-renderiza o React.
useAnimatedStyle retorna um objeto de estilo que é recalculado a cada frame dentro de um worklet.
Importe Animated de react-native-reanimated, não de react-native, ao usar estilos controlados por worklets.
Relacionado: Worklets & Bundle Mode - como os worklets são compilados e depurados
Springs parecem responsivos quando vinculados à velocidade de liberação - o padrão do Reanimated para UI interativa.
import { Pressable, Text } from "react-native" ;
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated" ;
export function SpringChip ({ label } : { label : string }) {
const scale = useSharedValue ( 1 );
const style = useAnimatedStyle (() => ({
transform: [{ scale: scale.value }],
}));
return (
< Pressable
onPressIn = {() => {
scale.value = withSpring ( 0.94 , { damping: 15 , stiffness: 400 });
}}
onPressOut = {() => {
scale.value = withSpring ( 1 );
}}
>
< Animated.View style = {[{ padding: 12 , borderRadius: 999 , backgroundColor: "#dbeafe" }, style]}>
< Text >{label}</ Text >
</ Animated.View >
</ Pressable >
);
}
withSpring aceita damping, stiffness e mass - ajuste uma vez por sistema de design, reutilize através de constantes.
Os manipuladores de Pressable atualizam valores compartilhados diretamente - ainda bom para chips simples; pans usam Gesture Handler em vez disso.
Springs na UI thread permanecem suaves mesmo quando a JS está ocupada processando uma grande resposta de API.
Relacionado: react-native-gesture-handler - Gesture.Pan().onUpdate controlando o mesmo valor compartilhado
Entender onde os frames são executados evita "jank misterioso" em produção.
JS thread (React) UI thread (native)
───────────────── ───────────────────
Renderização React → commit Reconhecimento de gestos
setState a cada frame ❌ Atualizações de valor compartilhado ✅
Animated sem driver nativo ❌ useAnimatedStyle ✅
Lógica de negócios, rede withTiming / withSpring
// ❌ Executa na JS thread - compete com a reconciliação do React
const [ offset , setOffset ] = useState ( 0 );
onPanResponderMove : ( _ , g ) => setOffset (g.dx);
// ✅ Executa na UI thread - 60fps mesmo durante trabalho pesado na JS
const translateX = useSharedValue ( 0 );
Gesture. Pan (). onUpdate (( e ) => {
translateX.value = e.translationX;
});
60fps significa 16.7ms por frame - uma renderização JS cara durante um pan perde um frame visivelmente.
Use React DevTools e o monitor da JS thread; use o log de depuração do Reanimated para problemas de worklet.
Perfilar em um dispositivo Android de gama média - simuladores de iPhone mascaram a contenção da JS thread.
Relacionado: Melhores Práticas de Animação - checklist de perfil antes de lançar movimento | ../performance/best-practices/best-practices.md - matriz de dispositivos para revisão de desempenho
Versões da Stack: Esta página foi escrita para React 19.2.3 , React Native 0.86.0 e Expo SDK 57 (expo ~57.0.4).