Skip to Content
React Native10. Animations & gestes10.2 Reanimated : fondations

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;DRuseSharedValue (la valeur qui vit sur le thread UI), useAnimatedStyle (le style-worklet qui la consomme), les fonctions de transition withTiming (durée) / withSpring (physique) / withSequence/withDelay (composition), et runOnJS (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 .value ne 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.value pendant 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, ou useDerivedValue + 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.