expo-widgets : Widgets et Live Activities en React Native — Le Guide Complet

Apprenez à créer des widgets d'écran d'accueil et des Live Activities en React Native avec expo-widgets. Configuration, code, interactions et Live Activities avec Expo SDK 55.

Les widgets d'écran d'accueil et les Live Activities comptent parmi les fonctionnalités les plus visibles qu'une application iOS peut proposer. Un widget bien pensé, c'est votre appli qui reste présente dans le quotidien de vos utilisateurs — même quand elle est fermée. Et honnêtement, ça change tout en termes d'engagement.

Sauf que jusqu'à récemment, créer un widget depuis une application React Native, c'était un vrai parcours du combattant : une cible Xcode séparée, des App Groups à configurer pour le partage de données, du code SwiftUI à écrire, et la joie de maintenir cette extension synchronisée avec le reste de votre app. Bref, pas mal de friction.

En mars 2026, Expo a lancé expo-widgets — une bibliothèque en alpha qui permet de créer des widgets d'écran d'accueil et des Live Activities avec des composants Expo UI, sans écrire une seule ligne de code natif. Alors, voyons ensemble comment ça marche.

Comment fonctionne expo-widgets sous le capot

Avant de plonger dans le code, prenons un moment pour comprendre l'architecture. Les extensions de widgets ne peuvent pas exécuter React Native au moment du rendu — elles sont rendues par le système à la demande, avec un budget de temps très serré. Votre app n'est peut-être même pas en cours d'exécution à ce moment-là.

C'est là qu'intervient @expo/ui.

Cette librairie expose des composants React (Text, VStack, HStack, Image) qui sont mappés directement vers des primitives SwiftUI. Quand le système demande une timeline de widget, votre composant s'exécute dans un runtime JavaScript séparé et produit un arbre de layout @expo/ui. Le côté natif utilise ensuite cette description pour reconstruire l'interface avec des vues SwiftUI — aucun React Native n'est impliqué dans le rendu réel. C'est une subtilité importante à retenir.

Le config plugin se charge de générer automatiquement :

  • La cible Widget Extension dans Xcode
  • La configuration de l'App Group pour le partage de données
  • Tous les fichiers natifs nécessaires pendant le prebuild

Prérequis et installation

Avant de commencer, vérifiez que vous avez bien tout ça :

  • Expo SDK 55 ou supérieur — c'est obligatoire car il utilise React Native 0.83 et la New Architecture est toujours activée
  • Un development build — expo-widgets ne fonctionne pas dans Expo Go (oui, c'est un peu dommage)
  • iOS 16+ pour les widgets, iOS 16.1+ pour les Live Activities
  • Un compte Apple Developer si vous voulez tester sur un appareil physique ou déployer via TestFlight

L'installation est simple :

npx expo install expo-widgets

Configuration du Config Plugin

La configuration se fait dans votre fichier app.json. Le config plugin gère la création de la cible Xcode, les entitlements et l'App Group — pas besoin de toucher à Xcode manuellement :

{
  "expo": {
    "plugins": [
      [
        "expo-widgets",
        {
          "bundleIdentifier": "com.monapp.widgets",
          "groupIdentifier": "group.com.monapp",
          "enablePushNotifications": true,
          "widgets": [
            {
              "name": "MeteoWidget",
              "displayName": "Météo",
              "description": "Affiche la météo actuelle en un coup d'œil",
              "supportedFamilies": [
                "systemSmall",
                "systemMedium",
                "systemLarge"
              ]
            }
          ]
        }
      ]
    ]
  }
}

Propriétés de configuration importantes

Voici les propriétés principales à connaître :

  • bundleIdentifier — L'identifiant du bundle de l'extension widget. Par défaut : <appBundleId>.ExpoWidgetsTarget
  • groupIdentifier — L'App Group pour le partage de données entre l'app et les widgets. Par défaut : group.<appBundleId>
  • enablePushNotifications — Active l'entitlement aps-environment pour les mises à jour push des Live Activities
  • widgets — Un tableau de configurations de widgets

Familles de tailles supportées

Chaque widget peut être configuré pour une ou plusieurs tailles. Personnellement, je recommande de toujours supporter au minimum systemSmall et systemMedium — ce sont de loin les plus utilisés :

  • systemSmall — Grille 2×2, idéal pour un aperçu rapide
  • systemMedium — Grille 4×2, permet d'afficher plus de détails
  • systemLarge — Grille 4×4, pour les contenus riches
  • systemExtraLarge — Grille 6×4, iPad uniquement
  • accessoryCircular — Widget circulaire sur l'écran de verrouillage
  • accessoryRectangular — Widget rectangulaire sur l'écran de verrouillage
  • accessoryInline — Texte inline sur l'écran de verrouillage

Créer votre premier widget

Bon, passons à la pratique. Créons un widget compteur simple pour comprendre les bases. Deux choses à retenir : chaque widget doit inclure la directive 'widget', et le nom passé à createWidget doit correspondre exactement au champ name dans votre configuration.

import { Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, WidgetBase } from 'expo-widgets';

type CompteurWidgetProps = {
  count: number;
};

const CompteurWidget = (props: WidgetBase<CompteurWidgetProps>) => {
  'widget';
  return (
    <VStack spacing={8}>
      <Text
        modifiers={[
          font({ size: 48 }),
        ]}
      >
        ☕
      </Text>
      <Text
        modifiers={[
          font({ size: 32, weight: 'bold' }),
          foregroundStyle('#333333'),
        ]}
      >
        {props.count}
      </Text>
    </VStack>
  );
};

export default createWidget('CompteurWidget', CompteurWidget);

Comprendre WidgetBase

Le type WidgetBase<T> étend vos props personnalisées avec deux propriétés injectées automatiquement :

  • date: Date — La date de l'entrée timeline en cours
  • family: WidgetFamily — La taille actuelle du widget (par exemple 'systemSmall')

C'est pratique car ça vous permet d'adapter le rendu selon le contexte sans props supplémentaires.

Mettre à jour un widget depuis l'application

Une fois le widget créé, vous pouvez le mettre à jour de deux façons depuis votre app React Native. Et c'est là que la magie opère vraiment.

Mise à jour instantanée avec updateSnapshot

La méthode updateSnapshot crée une timeline avec une seule entrée qui s'affiche immédiatement. C'est la plus simple à utiliser :

import CompteurWidget from './widgets/CompteurWidget';

// Mettre à jour immédiatement le widget
CompteurWidget.updateSnapshot({ count: 5 });

Planifier des mises à jour avec updateTimeline

Pour planifier des mises à jour futures, utilisez updateTimeline en fournissant un tableau d'entrées datées :

CompteurWidget.updateTimeline([
  { date: new Date(), props: { count: 1 } },
  { date: new Date(Date.now() + 3600000), props: { count: 2 } },
  { date: new Date(Date.now() + 7200000), props: { count: 3 } },
]);

Un point important : les widgets iOS ne se rafraîchissent pas à la demande. Vous fournissez une timeline à iOS, et c'est le système qui décide du moment exact de la mise à jour. Ça peut être un peu frustrant au début, mais c'est conçu comme ça pour préserver la batterie.

Autres méthodes disponibles

  • reload() — Force un rafraîchissement du contenu/timeline du widget
  • getTimeline() — Retourne les entrées de timeline actuelles (retourne une Promise)

Créer un widget adaptatif multi-tailles

L'un des gros avantages d'expo-widgets, c'est la possibilité de créer des layouts différents selon la taille du widget grâce à la propriété family. Voici un exemple concret avec un widget météo :

import { HStack, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';
import { createWidget, WidgetBase } from 'expo-widgets';

type MeteoWidgetProps = {
  temperature: number;
  condition: string;
};

const MeteoWidget = (props: WidgetBase<MeteoWidgetProps>) => {
  'widget';

  if (props.family === 'systemSmall') {
    return (
      <VStack>
        <Text modifiers={[font({ size: 36, weight: 'bold' })]}>
          {props.temperature}°
        </Text>
      </VStack>
    );
  }

  if (props.family === 'systemMedium') {
    return (
      <HStack modifiers={[padding({ all: 12 })]}>
        <Text modifiers={[font({ size: 28, weight: 'bold' })]}>
          {props.temperature}°
        </Text>
        <Text modifiers={[foregroundStyle('#666666')]}>
          {props.condition}
        </Text>
      </HStack>
    );
  }

  return (
    <VStack modifiers={[padding({ all: 16 })]}>
      <Text modifiers={[font({ size: 24, weight: 'bold' })]}>
        Température : {props.temperature}°
      </Text>
      <Text>Condition : {props.condition}</Text>
      <Text modifiers={[foregroundStyle('#999999'), font({ size: 12 })]}>
        Mis à jour : {props.date.toLocaleTimeString()}
      </Text>
    </VStack>
  );
};

export default createWidget('MeteoWidget', MeteoWidget);

Les Live Activities : informations en temps réel

Les Live Activities, c'est probablement la partie la plus excitante. Elles affichent des informations en temps réel sur l'écran de verrouillage et dans la Dynamic Island (sur les appareils qui la supportent). Idéales pour tout ce qui est « processus en cours » : livraisons, scores sportifs, statut de vol, suivi de course...

Créer une Live Activity

La création se fait avec createLiveActivity. Contrairement aux widgets classiques, une Live Activity retourne un objet avec plusieurs zones de layout — et il y en a pas mal :

import { Image, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';
import { createLiveActivity } from 'expo-widgets';

type LivraisonActivityProps = {
  etaMinutes: number;
  statut: string;
};

const LivraisonActivity = (props: LivraisonActivityProps) => {
  'widget';
  return {
    banner: (
      <VStack modifiers={[padding({ all: 12 })]}>
        <Text
          modifiers={[
            font({ weight: 'bold' }),
            foregroundStyle('#007AFF'),
          ]}
        >
          {props.statut}
        </Text>
        <Text>Arrivée estimée : {props.etaMinutes} minutes</Text>
      </VStack>
    ),
    compactLeading: (
      <Image systemName="box.truck.fill" color="#007AFF" />
    ),
    compactTrailing: (
      <Text>{props.etaMinutes} min</Text>
    ),
    minimal: (
      <Image systemName="box.truck.fill" color="#007AFF" />
    ),
    expandedLeading: (
      <VStack modifiers={[padding({ all: 12 })]}>
        <Image systemName="box.truck.fill" color="#007AFF" />
        <Text modifiers={[font({ size: 12 })]}>En livraison</Text>
      </VStack>
    ),
    expandedTrailing: (
      <VStack modifiers={[padding({ all: 12 })]}>
        <Text modifiers={[font({ weight: 'bold', size: 20 })]}>
          {props.etaMinutes}
        </Text>
        <Text modifiers={[font({ size: 12 })]}>minutes</Text>
      </VStack>
    ),
    expandedBottom: (
      <VStack modifiers={[padding({ all: 12 })]}>
        <Text>Livreur : Jean Dupont</Text>
        <Text>Commande #12345</Text>
      </VStack>
    ),
  };
};

export default createLiveActivity(
  'LivraisonActivity',
  LivraisonActivity
);

Zones de layout d'une Live Activity

Chaque Live Activity peut définir jusqu'à 9 zones de layout. Ça fait beaucoup, mais en pratique vous n'aurez pas toujours besoin de toutes les définir :

  • banner (obligatoire) — Le contenu principal sur l'écran de verrouillage et le Centre de notifications
  • bannerSmall — Version pour CarPlay et WatchOS, se rabat sur banner si non défini
  • compactLeading — Côté gauche de la Dynamic Island en mode compact
  • compactTrailing — Côté droit de la Dynamic Island en mode compact
  • minimal — La plus petite forme de la Dynamic Island
  • expandedLeading / expandedTrailing / expandedCenter / expandedBottom — Les zones de la Dynamic Island en mode étendu

Démarrer et mettre à jour une Live Activity

L'API est assez intuitive. Vous démarrez, vous mettez à jour, vous terminez :

import LivraisonActivity from './activities/LivraisonActivity';

// Démarrer la Live Activity
const instance = LivraisonActivity.start({
  etaMinutes: 15,
  statut: 'Votre livraison est en route',
});

// Mettre à jour plus tard
instance.update({
  etaMinutes: 5,
  statut: 'Le livreur est presque arrivé !',
});

// Terminer la Live Activity
instance.end('default');

Notifications push pour Live Activities

Pour envoyer des mises à jour depuis votre serveur (et c'est souvent ce qu'on veut faire en production), vous pouvez récupérer le push token APNs :

// Récupérer le token push
const token = await instance.getPushToken();

// Ou écouter les changements de token
const subscription = instance.addPushTokenListener((event) => {
  console.log('Nouveau token :', event.pushToken);
  // Envoyer ce token à votre serveur
});

// Nettoyage
subscription.remove();

N'oubliez pas d'activer enablePushNotifications: true dans la config du plugin — sans ça, les push tokens ne seront pas disponibles.

Interactions utilisateur dans les widgets

Voilà une fonctionnalité qui m'a vraiment impressionné : expo-widgets permet de gérer les interactions utilisateur directement dans les widgets, sans lancer l'application. Le composant Button peut retourner une mise à jour partielle de l'état :

import { Button, Text, VStack } from '@expo/ui/swift-ui';
import { foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, WidgetBase } from 'expo-widgets';

type HabitWidgetProps = { count: number };

const HabitWidget = (props: WidgetBase<HabitWidgetProps>) => {
  'widget';
  return (
    <VStack spacing={8}>
      <Text>Verres d'eau : {props.count}</Text>
      <Button
        modifiers={[foregroundStyle('#007AFF')]}
        label="+1 verre"
        target="increment"
        onPress={() => ({ count: props.count + 1 })}
      />
    </VStack>
  );
};

export default createWidget('HabitWidget', HabitWidget);

La fonction onPress retourne un objet de mise à jour partielle qui est fusionné avec l'état actuel et re-rendu — aucun lancement de l'app n'est nécessaire. Vraiment élégant comme approche.

Vous pouvez aussi écouter les interactions depuis votre application principale :

import { addUserInteractionListener } from 'expo-widgets';

const subscription = addUserInteractionListener((event) => {
  console.log('Interaction :', event.target, event.source);
});

// Nettoyage
subscription.remove();

Cas d'usage recommandés

Si vous hésitez entre un widget et une Live Activity, voici comment je vois les choses.

Widgets — pour les « consultations rapides »

  • Événements du calendrier
  • Conditions météo
  • Compteur de tâches à faire
  • Suivi d'habitudes (streak)
  • Statistiques rapides (pas, calories, etc.)

Live Activities — pour les « processus en cours »

  • Estimation d'arrivée d'une livraison
  • Scores sportifs en direct
  • Statut de vol en temps réel
  • Suivi de trajet (VTC, taxi)
  • Minuteries et chronomètres

En gros : si l'information est « statique » et consultée ponctuellement, c'est un widget. Si elle évolue en continu pendant un laps de temps défini, c'est une Live Activity.

Limitations actuelles à connaître

Comme expo-widgets est en alpha, il y a quelques limitations importantes. Mieux vaut les connaître avant de se lancer :

  • iOS uniquement — Toutes les API sont exclusives à iOS. Pour Android, il faut toujours écrire du code natif Java/Kotlin
  • Pas de support dans Expo Go — Un development build est obligatoire, pas moyen de contourner ça
  • Images limitées — Le support des images dans @expo/ui est encore expérimental
  • Live Activities limitées à 12h — iOS termine automatiquement les activités qui dépassent cette durée
  • Pas de requêtes réseau dans les Live Activities — Les images doivent être pré-chargées via l'App Group
  • API sujette à changements — Des breaking changes sont possibles à tout moment

expo-widgets vs Voltra : quelle alternative choisir ?

Callstack a sorti Voltra, une alternative pour les widgets et Live Activities en React Native. La question se pose forcément : lequel choisir ?

Les principales différences :

  • expo-widgets utilise SwiftUI et ses modifiers directement via @expo/ui
  • Voltra utilise ses propres composants avec un sous-ensemble du style prop React Native (plus familier si vous venez du monde RN pur)
  • Voltra a actuellement une documentation un peu plus fournie et davantage d'exemples
  • expo-widgets est mieux intégré dans l'écosystème Expo avec la Continuous Native Generation

Mon avis ? Si vous utilisez déjà Expo et le workflow CNG, expo-widgets est le choix naturel. Si vous avez besoin d'une solution plus mature tout de suite, jetez un œil à Voltra.

FAQ

Peut-on utiliser expo-widgets avec Expo Go ?

Non. expo-widgets nécessite un development build. Les extensions de widgets doivent être compilées nativement, ce qui n'est pas possible avec Expo Go. Utilisez npx expo run:ios ou EAS Build pour tester vos widgets.

Les widgets expo-widgets fonctionnent-ils sur Android ?

Pas pour l'instant. expo-widgets est actuellement exclusif à iOS. Pour les widgets Android, il faut toujours écrire du code natif en Java ou Kotlin avec un development build personnalisé. Le support Android pourrait arriver dans une future version, mais rien n'est confirmé.

Comment partager des données entre l'application et le widget ?

Via les App Groups — des conteneurs partagés configurés automatiquement par le config plugin avec le groupIdentifier. Utilisez updateSnapshot ou updateTimeline pour pousser les données vers le widget depuis votre app.

Quelle est la durée maximale d'une Live Activity ?

12 heures, c'est la limite imposée par iOS. Si votre processus dépasse cette durée, prévoyez de relancer une nouvelle activité ou de rediriger l'utilisateur vers l'app.

expo-widgets est-il prêt pour la production ?

Franchement, pas encore tout à fait. La bibliothèque est en alpha en mars 2026 et l'API est sujette à des changements cassants. C'est parfait pour des projets perso et des prototypes, mais pour une app en production critique, évaluez bien les risques et prévoyez de suivre les mises à jour de près.

À propos de l'auteur Editorial Team

Our team of expert writers and editors.