Chapitre 10.2 — Reanimated : les fondations
Où on en est : le pourquoi est clair. Place au comment : les quatre briques de Reanimated qui couvrent 95 % des animations d’une app.
⏱️ TL;DR —
useSharedValue(la valeur qui vit sur le thread UI),useAnimatedStyle(le style-worklet qui la consomme), les fonctions de transitionwithTiming(durée) /withSpring(physique) /withSequence/withDelay(composition), etrunOnJS(rappeler le monde React depuis un worklet). On anime le AnswerButton, la ProgressBar, et le shake d’une mauvaise réponse.
🎯 Objectifs
- Manipuler shared values et animated styles sans erreur de thread.
- Choisir timing vs spring (et configurer les deux).
- Composer : séquences, délais, répétitions — le shake d’erreur.
- Rappeler du code React depuis une animation (runOnJS) — proprement.
Les deux briques de base
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'
function PopButton({ children, onPress }: Props) {
const scale = useSharedValue(1) // 1. la valeur, côté thread UI
const animatedStyle = useAnimatedStyle(() => ({ // 2. le style-worklet qui la lit
transform: [{ scale: scale.value }],
}))
return (
<Animated.View style={animatedStyle}>
<Pressable
onPressIn={() => { scale.value = withSpring(0.96) }}
onPressOut={() => { scale.value = withSpring(1) }}
onPress={onPress}
>
{children}
</Pressable>
</Animated.View>
)
}Le modèle mental :
useSharedValue(1)crée une valeur accessible des DEUX mondes : le JS écrit (scale.value = …), le thread UI lit à chaque frame. Modifier.valuene re-rend RIEN — c’est le point : l’animation contourne React.useAnimatedStyle: la fonction passée est un worklet — Reanimated la compile pour le thread UI et la ré-exécute chaque fois qu’une shared value lue dedans change. Vous écrivez du JS ; il tourne côté natif.Animated.View(et .Text, .ScrollView…) : les hôtes qui acceptent un animated style — ils cohabitent avec className (structure NativeWind + animation Reanimated, le partage du ch. 4.4).
timing vs spring
import { withTiming, withSpring, Easing } from 'react-native-reanimated'
// TIMING : durée + courbe — pour les valeurs « mécaniques » (progression, opacité)
progress.value = withTiming(0, { duration: 20_000, easing: Easing.linear })
// SPRING : physique (raideur/amortissement) — pour tout ce qui « bouge » à l'écran
scale.value = withSpring(1, { damping: 15, stiffness: 180 })La règle de choix, simple et fiable : ce qui représente une quantité (une progression, un
pourcentage, un fondu) → timing ; ce qui représente un objet physique (un bouton, une
carte, un élément qui se déplace) → spring. Le spring n’a pas de durée — il a un caractère :
damping bas = rebondi joueur, haut = ferme. Drill : boutons fermes (jeu de réflexes, pas de
gélatine).
Composer : le shake d’erreur
Une mauvaise réponse secoue le bouton — la composition en une expression :
// components/AnswerButton.tsx — ajout
const shakeX = useSharedValue(0)
const shakeStyle = useAnimatedStyle(() => ({
transform: [{ translateX: shakeX.value }],
}))
// À déclencher quand state passe à 'incorrect' :
function shake() {
shakeX.value = withSequence(
withTiming(-8, { duration: 40 }),
withRepeat(withTiming(8, { duration: 80 }), 3, true), // 3 allers-retours
withTiming(0, { duration: 40 })
)
}
useEffect(() => {
if (state === 'incorrect') shake()
}, [state])withSequence enchaîne, withRepeat(…, 3, true) fait l’aller-retour, withDelay(ms, anim)
temporise. Ces combinateurs remplacent 90 % des « timelines » des libs web.
runOnJS : le retour vers React
Un worklet tourne sur le thread UI — il ne peut PAS appeler directement du code React (setState, router, store). Le pont officiel :
import { runOnJS } from 'react-native-reanimated'
// Naviguer QUAND l'animation de sortie est finie :
opacity.value = withTiming(0, { duration: 250 }, finished => {
if (finished) runOnJS(goToResults)() // goToResults est une fonction JS normale
})Le callback de fin d’animation EST un worklet (il s’exécute côté UI) — d’où le runOnJS pour
repasser la main. Oublier ce wrapper = l’erreur la plus fréquente de Reanimated (le message
« Tried to synchronously call a non-worklet function on the UI thread » — retenez-le).
⚠️ Piège — Ne LISEZ jamais
sharedValue.valuependant le render d’un composant (<Text>{scale.value}</Text>) : la valeur vit sur l’autre thread, React n’est pas notifié de ses changements — affichage figé et warning. Pour AFFICHER une valeur animée :useAnimatedStyle/useAnimatedProps, ouuseDerivedValue+ le composant ReText de react-native-redash (on le fera pour les secondes du timer, ch. 10.3).
💡 Pour un dev Next.js — Récapitulatif mental :
useSharedValue≈ un useRef dont les mutations pilotent le compositor ;useAnimatedStyle≈ une CSS custom property mise à jour à 120 Hz ; withSpring ≈ la spring de Framer Motion. La grosse différence : la frontière de THREAD est explicite (worklets, runOnJS) — le web vous la cachait.
✏️ Exercices
1. La ProgressBar du kit (ch. 4.5) doit s’animer : la largeur suit ratio avec un timing
de 200 ms au lieu de sauter. Migrez-la SANS changer son API.
✅ Solution
export function ProgressBar({ ratio }: { ratio: number }) {
const w = useSharedValue(ratio)
useEffect(() => { w.value = withTiming(ratio, { duration: 200 }) }, [ratio])
const style = useAnimatedStyle(() => ({ width: `${w.value * 100}%` }))
return (
<View className="h-2 overflow-hidden rounded-full bg-ink-700/60">
<Animated.View className="h-full rounded-full bg-brand" style={style} />
</View>
)
}L’API (ratio) est inchangée : les consommateurs ne savent pas qu’elle est devenue fluide.
Le useEffect fait le pont prop → shared value : LE pattern d’intégration React/Reanimated.
2. Le PopButton du chapitre : ajoutez un léger « overshoot » à la relâche (le bouton remonte à 1,03 puis se pose à 1). Deux écritures possibles — spring seul ou séquence.
✅ Solution
Spring seul : scale.value = withSpring(1, { damping: 9, stiffness: 200 }) — l’overshoot est
DANS la physique (damping bas → dépassement naturel). Séquence explicite :
withSequence(withTiming(1.03, { duration: 90 }), withTiming(1, { duration: 110 })).
Préférez le spring : un seul terme, interruptible proprement (un nouveau press au milieu
re-cible sans à-coup — les séquences, elles, repartent de zéro).
3. Provoquez volontairement l’erreur runOnJS (appeler router.push directement dans un callback de fin d’animation), lisez le message, corrigez. Pourquoi l’erreur ne survient-elle qu’À L’EXÉCUTION du callback ?
✅ Solution
Le callback n’est compilé en worklet et exécuté côté UI qu’au moment où l’animation finit —
le JS ne peut pas détecter statiquement l’appel illégal (router.push est une fonction comme
une autre à la compilation). D’où : erreur au runtime, dans le callback. Correction :
runOnJS(router.push)('/quiz/results') — noter la syntaxe : runOnJS(fn)(args), le wrapper
retourne une version appelable du worklet.
🧠 Quiz
1. Que se passe-t-il côté React quand on écrit sharedValue.value = 5 ?
Réponse
Rien — aucun re-render. La valeur vit côté UI thread ; seuls les worklets qui la lisent (useAnimatedStyle) réagissent. C’est le mécanisme qui contourne React pour tenir les 16 ms.
2. Quand choisir withTiming, quand withSpring ?
Réponse
Timing pour les quantités (progression, opacité — durée + easing) ; spring pour les objets (boutons, cartes — physique damping/stiffness, interruptible naturellement).
3. Citez les trois combinateurs de composition.
Réponse
withSequence (enchaîner), withRepeat (répéter, yo-yo optionnel), withDelay (temporiser).
4. Pourquoi runOnJS existe-t-il ?
Réponse
Les worklets (styles, callbacks de fin) s’exécutent sur le thread UI, qui ne peut pas appeler les fonctions JS/React directement — runOnJS repasse l’appel au thread JS.
5. Comment afficher la valeur d’une shared value dans un Text ?
Réponse
Jamais en la lisant au render (pas de notification React) — via useAnimatedProps sur un composant texte animé (ReText de redash) ou en dérivant un état React aux moments utiles.
👉 Chapitre suivant : 10.3 — Le timer circulaire — SVG + Reanimated : la pièce maîtresse visuelle de Drill.