Accueil / Articles / Gestion des états en couches dans une application de streaming React : Context, Redux Toolkit et RTK Query

Gestion des états en couches dans une application de streaming React : Context, Redux Toolkit et RTK Query

Suivez un frontend de diffusion vidéo, depuis l’utilisation de prop drilling jusqu’aux providers imbriqués, en passant par les slices Redux Toolkit et RTK Query, et découvrez quel type d’état convient à chaque outil.

1694 mots

Presque tous les applications React en développement arrivent au stade où le composant racine est enfoui sous une pile de fournisseurs, et un minuteur de lecture réaffiche parfois la sonnerie de notification. La solution ne réside rarement dans une seule bibliothèque ; il s’agit plutôt de comprendre que différents types d’état nécessitent des structures adaptées. Cette présentation utilise un frontend de streaming vidéo comme étude de cas, en passant par l’utilisation des props jusqu’à l’API Context, puis Redux Toolkit et RTK Query, pour finir par une règle pratique permettant de choisir entre eux.

L’exemple : un frontend de streaming

Imaginons les fonctionnalités principales d’un service de streaming d’animes ou de vidéos :

  • Connexion et abonnements, gratuits ou premium
  • Une liste de visionnage ainsi qu’une ligne « Continuer à regarder »
  • L’état du lecteur : l’épisode actuel, le niveau de progression de la lecture et les paramètres de qualité
  • Naviguer dans le catalogue et effectuer des recherches avec des filtres de genre
  • notifications pour les nouveaux épisodes et rappels de souscription
  • Chacun d’eux doit exister quelque part dans l’arbre des composants, et plusieurs composants situés loin les uns des autres ont besoin de les lire. C’est précisément dans cette combinaison que les décisions prises au début concernant l’état se révèlent utiles ou se transforment en des refacteurs lents et coûteux des mois plus tard.

    Étape un : propagation descendante des propriétés

    Le premier réflexe est de placer l’état dans le plus proche ancêtre commun et de le transmettre vers le bas. Pour les petites applications, c’est la bonne approche. Cependant, dans une interface de streaming, le chemin allant de la racine jusqu’à un bouton peut être long :

    App → MainLayout → ContentSection → AnimeGrid → AnimeCard → PlayButton

    Supposons que PlayButton doive connaître le niveau de abonnement pour déterminer s’il faut afficher un icône de verrou premium. Ce niveau doit être transmis via MainLayout, ContentSection et AnimeGrid, qui ne l’utilisent pas pour autant. Ils n’existent dans cette chaîne que comme intermédiaires.

    Un seul prop foré est acceptable. Les problèmes commencent lorsque une deuxième valeur, non liée, comme la liste de suivi, doit emprunter le même chemin. Chaque composant intermédiaire contient alors des props qu’il ne comprend pas, ce qui rend leur réutilisation ailleurs plus difficile, leurs tests en isolation plus compliqués et leur compréhension plus ardue. Avant de recourir à une bibliothèque, il vaut la peine de lire pourquoi le simple forage de props n’est pas une raison d’installer Redux ou Zustand ; la composition permet souvent de raccourcir ces chaînes. Cependant, dans cette application, les données sont véritablement globales.

    Deuxième étape : Le contexte et la pyramide des fournisseurs

    L’API Context de React représente la prochaine étape naturelle. Vous créez un AuthContext, vous enveloppez l’arbre dans un AuthProvider, et le PlayButton lit directement cette information à l’aide de useContext(AuthContext). Le problème de la hiérarchie imbriquée disparaît ainsi.

    Puis apparaissent les autres problématiques globales, chacune ayant son propre fournisseur, et la racine du système commence à ressembler à ceci :

    <AuthProvider>
      <SubscriptionProvider>
        <WatchlistProvider>
          <PlayerProvider>
            <NotificationProvider>
              <ThemeProvider>
                <App />
              </ThemeProvider>
            </NotificationProvider>
          </PlayerProvider>
        </WatchlistProvider>
      </SubscriptionProvider>
    </AuthProvider>
    

    C’est ce que les développeurs appellent l’enfer des fournisseurs : la racine de l’application devient un ensemble d’ensembles imbriqués, chacun ajoutant une couche d’indirection. Le bruit visuel n’est que le moindre des problèmes.

    Le débogage nécessite une carte

    Lorsque la lecture se comporte de manière anormale, il faut d’abord savoir quel fournisseur gère cet état, puis suivre la hiérarchie pour identifier l’endroit où il change. L’arbre lui-même ne vous renseigne pas.

    Chaque mise à jour atteint tous les consommateurs

    Un changement de valeur dans le contexte force à re-render tous les composants qui utilisent ce contexte, même ceux qui n’en exploitent qu’une petite partie. Pour continuer à suivre la vidéo, il est nécessaire de sauvegarder playbackProgress tous les quelques secondes. Si cette valeur se trouve dans PlayerContext aux côtés des paramètres de qualité, un composant qui ne sert qu’à afficher l’icône de qualité doit néanmoins être re-render à chaque mise à jour.

    L’ordre des fournisseurs devient un contrat implicite

    WatchlistProvider a besoin de l’ID de l’utilisateur connecté fourni par AuthProvider, ce qui impose que celui-ci soit imbriqué à l’intérieur de AuthProvider. Rien dans le JSX ne rend cette dépendance explicite, et un changement d’ordre des fournisseurs lors d’une refonte peut endommager l’application de manière difficile à diagnostiquer.

    Un symptôme réel : une équipe combine un contexte de joueur et un contexte de notification, et l’analyse révèle que les mises à jour d’avancement déclenchent une nouvelle affichage des notifications. Rien n’est visiblement cassé, mais le React Profiler montre beaucoup plus d’affichages que ce dont l’interface a besoin.

    Le contexte peut être ajusté, par exemple en séparant les valeurs qui changent fréquemment dans leur propre contexte ou en mémorisant les valeurs des fournisseurs, mais chaque solution de contournement ajoute davantage de fournisseurs et plus de complexité. À ce stade, un store dédié est souvent plus simple.

    Étape trois : les slices de Redux Toolkit

    De nombreuses équipes recourent désormais à des stores plus légers tels que Zustand à ce stade. Redux Toolkit reste un choix solide lorsque l’on dispose de plusieurs slices d’état liés, que l’on souhaite un débogage en mode « voyage dans le temps » et que l’on privilégie une source unique et prévisible de vérité, car il élimine la plupart des éléments redondants qui rendaient le Redux classique difficile à utiliser.

    Chaque préoccupation devient une tranche. La tranche du joueur ci-dessous contient l’épisode actuel, son niveau de progression et sa qualité, et définit des réducteurs permettant de modifier l’épisode et de mettre à jour la progression. Notez que ces réducteurs semblent modifier directement state ; Redux Toolkit utilise Immer en arrière-plan, ce qui permet à ces affectations de générer un nouvel état immuable en toute sécurité :

    // playerSlice.js
    const playerSlice = createSlice({
      name: 'player',
      initialState: {
        currentEpisode: null,
        playbackProgress: 0,
        quality: '1080p',
      },
      reducers: {
        setEpisode: (state, action) => {
          state.currentEpisode = action.payload;
        },
        updateProgress: (state, action) => {
          state.playbackProgress = action.payload;
        },
      },
    });
    

    La structure en niveaux a disparu. Un seul <Provider store={store}> entoure l’application, et les composants ne lisent que ce dont ils ont besoin grâce à useSelector. Comme useSelector compare la valeur sélectionnée entre les rendus, le PlayButton qui choisit un niveau de abonnement se rérend uniquement lorsque ce dernier change, et non à chaque mise à jour du niveau de progression. Ce modèle d’abonnement sélectif élimine la plupart des rendus inutiles générés par la version basée sur Context.

    L’autre avantage majeur est Redux DevTools. En parcourant pas à pas chaque action envoyée, comme la pression sur « jouer », la mise à jour du niveau de progression ou le changement d’épisode, et en observant précisément comment l’état évolue, il devient beaucoup plus facile de diagnostiquer les bugs liés à la lecture que de suivre les valeurs à travers une pyramide de fournisseurs.

    Étape quatre : RTK Query pour l’état du serveur

    Une grande partie de la complexité de l’application ne concerne pas du tout l’état de l’interface utilisateur. Il s’agit de l’état du serveur : le catalogue, les résultats de recherche, les détails des épisodes ainsi que la liste de suivi stockés sur le backend. L’approche traditionnelle combine useEffect avec plusieurs appels à useState par requête pour suivre manuellement les données, le chargement et les erreurs, ce qui entraîne souvent des conditions de concurrence et des requêtes redondantes.

    RTK Query remplace cela par une partie API. La définition ci-dessous définit une URL de base et déclare deux points d’entrée de requête : l’un pour les anime filtrés par genre et l’autre pour les détails des épisodes :

    export const catalogApi = createApi({
      reducerPath: 'catalogApi',
      baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
      endpoints: (builder) => ({
        getAnimeList: builder.query({
          query: (genre) => `/anime?genre=${genre}`,
        }),
        getEpisodeDetails: builder.query({
          query: (episodeId) => `/episodes/${episodeId}`,
        }),
      }),
    });
    

    RTK Query génère un hook React pour chaque point d’entrée, nommé d’après celui-ci, que l’on exporte depuis la partie API :

    export const { useGetAnimeListQuery, useGetEpisodeDetailsQuery } = catalogApi;
    

    Une composante récupère alors les données, ainsi que l’état de chargement et d’erreur, en une seule ligne :

    const { data: animeList, isLoading, error } = useGetAnimeListQuery('action');
    

    Il n’y a ni effet écrit manuellement ni d’indicateur de chargement personnalisé. La fonctionnalité principale est le cacheage : si un utilisateur ouvre la section des genres, quitte l’application puis y revient, la liste en mémoire apparaît immédiatement, tandis que RTK Query refait la requête en arrière-plan si les données sont obsolètes. Les demandes identiques provenant de plusieurs composantes partagent une seule requête réseau.

    Pour la ligne de suivi continu, l’invalidation basée sur des étiquettes permet de maintenir une interface utilisateur précise : les requêtes indiquent ce qu’elles fournissent via providesTags, tandis que les mutations indiquent ce qu’elles modifient avec invalidatesTags. Lorsqu’une mutation de mise à jour du statut s’exécute, RTK Query recharge automatiquement les requêtes concernées, de sorte qu’aucun composant n’a besoin de déclencher manuellement une rechargement. Les étiquettes nécessitent une entrée tagTypes dans la partie API ainsi qu’un point de terminaison pour les mutations, éléments qui ne figurent pas dans l’exemple ci-dessus ; notre guide sur l’envoi de données via les mutations RTK Query explique ce point en détail. N’oubliez pas non plus que le réducteur et les middleware de la partie API doivent être ajoutés au store pour que le cache fonctionne.

    Associer chaque type d’état à un outil

    Pour une application comme celle-ci, une répartition logique se présente ainsi :

    • État local avec useState pour tout ce qui ne quitte jamais le composant : champs de formulaire, commutateurs, états d’hover et ouverts.
    • API Context pour des valeurs globales simples qui changent rarement, comme le thème ou la langue. Elle rencontre des difficultés lorsqu’il y a de nombreux contextes ou des valeurs mises à jour fréquemment.
    • Redux Toolkit pour des états clients complexes et interconnectés tels que l’authentification, le niveau de abonnement, l’état du lecteur et la liste d’observation, lus et écrits par de nombreux composants non liés.
    • RTK Query pour tout ce qui provient du backend, éliminant toute une catégorie de bugs : données obsolètes, concurrences de requêtes et requêtes redondantes.

    L’erreur courante consiste à considérer cela comme un choix binaire : soit tout est placé dans Context, soit tout est placé dans Redux. Ces outils résolvent des problèmes différents, et une application mature les combine généralement.

    Points clés

    • Le « prop drilling » est un signal indiquant qu’il faut d’abord restructurer ; on ne doit recourir à l’état global que lorsque les données sont réellement partagées entre des parties éloignées de l’arborescence.
    • Context réaffiche tous les consommateurs à chaque modification, ce qui en fait un mauvais choix pour des valeurs fréquemment mises à jour comme l’avancement de la lecture.
    • Les dépendances d’ordre cachées entre les fournisseurs représentent un risque de maintenance qui augmente avec chaque nouveau contexte.
    • Les abonnements basés sur des sélecteurs de Redux Toolkit ainsi que les outils DevTools permettent de gérer l’état client partagé plus rapidement et plus facilement à déboguer.
  • Évitez de laisser l’état du serveur influencer les effets personnalisés : le mécanisme de mise en cache et d’invalidation des étiquettes de RTK Query gère automatiquement la fraîcheur des données, à condition que le stockage et les étiquettes soient correctement configurés.