Startseite / Artikel / Schichtmodell im React-Streaming-App: Context, Redux Toolkit und RTK Query

Schichtmodell im React-Streaming-App: Context, Redux Toolkit und RTK Query

Verfolgen Sie einen Video-Streaming-Frontend-Entwicklungsprozess von Prop-Drilling über verschachtelte Provider bis hin zu Redux Toolkit Slices und RTK Query, und erfahren Sie, welche Art von Zustand in welches Werkzeug gehört.

1694 Wörter

Fast jede wachsende React-Anwendung erreicht den Punkt, an dem die Wurzelkomponente unter einer Ansammlung von Providers begraben ist und irgendein Wiedergabetimer die Benachrichtigungsglocke erneut darstellt. Die Lösung liegt selten in einer einzigen Bibliothek; vielmehr geht es darum zu erkennen, dass unterschiedliche Arten von Zustand unterschiedliche Aufbewahrungsorte benötigen. In dieser Anleitung wird ein Video-Streaming-Frontend als Fallstudie verwendet, wobei man von der Übermittlung von Eigenschaften über die Context API bis zu Redux Toolkit und RTK Query vorgeht, und am Ende gibt es eine praktische Regel zur Auswahl zwischen diesen Lösungen.

Das Beispiel: ein Streaming-Frontend

Stellen Sie sich die Kernfunktionen eines Anime- oder Video-Streaming-Dienstes vor:

  • Anmelden sowie Abonnementstufen – kostenlos oder Premium
  • Eine Wunschliste und eine Zeile zum „Weitersehen“
  • Zustand des Players: aktuelle Folge, Wiedergabefortschritt und Qualitäts-Einstellungen
  • Browsen des Katalogs sowie Suchfunktion mit Genre-Filtern
  • Benachrichtigungen für neue Episoden und Erinnerungen an die Abonnementverlängerung
  • Jedes dieser Elemente muss irgendwo im Komponentenbaum platziert werden, und mehrere weit voneinander entfernte Komponenten müssen darauf zugreifen können. Genau in dieser Kombination zeigen sich frühzeitige Entscheidungen bezüglich des Zustands entweder als vorteilhaft oder führen Monate später zu langsamen und kostspieligen Anpassungen.

    Erste Phase: Prop-Verteilung

    Der erste Impuls ist, den Zustand zum nächstgelegenen gemeinsamen Vorfahren zu heben und ihn weiterzugeben. Für kleine Anwendungen ist das die richtige Vorgehensweise. In einer Streaming-Oberfläche kann der Pfad vom Wurzelelement bis zu einem Button jedoch sehr lang sein:

    App → MainLayout → ContentSection → AnimeGrid → AnimeCard → PlayButton

    Nehmen wir an, PlayButton muss das Abonnementniveau kennen, um zu entscheiden, ob ein Premium-Sperre-Icon angezeigt werden soll. Das Niveau muss über MainLayout, ContentSection und AnimeGrid weitergeleitet werden, wobei keines davon es verwendet. Sie existieren in dieser Kette nur als Übermittler.

    Ein gebohrtes Prop ist noch erträglich. Das Problem beginnt, wenn ein zweiter, unabhängiger Wert wie die Watchlist denselben Weg nehmen muss. Jedes Zwischenkomponente trägt nun Props mit, die es nicht versteht, was es schwieriger macht, sie an anderer Stelle wiederverwenden, isoliert testen oder zu verstehen. Bevor man auf eine Bibliothek zurückgreift, lohnt es sich, zu lesen, warum allein das Prop-Bohren kein Grund ist, Redux oder Zustand zu installieren; Komposition verkürzt oft diese Ketten. In dieser App sind die Daten jedoch tatsächlich global.

    Zweiter Schritt: Context und die Provider-Pyramide

    Die Context API von React ist der natürliche nächste Schritt. Man erstellt einen AuthContext, umhüllt den Baum mit einem AuthProvider, und PlayButton liest den Zustand direkt mit useContext(AuthContext) ab. Das Bohren durch die Schichten entfällt.

    Dann treten weitere globale Probleme auf, jeweils mit ihrem eigenen Provider, und die Wurzelstruktur sieht dann so aus:

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

    Das ist das, was Entwickler als Provider-Hölle bezeichnen: Die Anwendungs-Wurzel wird zu einer Reihe von verschachtelten Umhüllungen, wodurch jede Schicht zusätzliche Indirektheit hinzufügt. Der visuelle Lärm ist das geringste der Probleme.

    Für das Debuggen braucht man eine Karte

    Wenn die Wiedergabe nicht wie erwartet funktioniert, muss man zunächst herausfinden, welcher Provider diesen Zustand verwaltet, und anschließend die Verschachtelung nachverfolgen, um den Punkt zu finden, an dem sich etwas ändert. Der Baum selbst gibt dazu keine Informationen.

    Jede Aktualisierung erreicht jeden Verbraucher

    Eine Änderung des Kontextwerts führt dazu, dass alle Komponenten, die diesen Kontext verwenden, neu gerendert werden – auch jene, die nur einen kleinen Teil davon nutzen. Um das Anschauen fortzusetzen, muss playbackProgress alle paar Sekunden gespeichert werden. Wenn dieser Wert zusammen mit der Qualitäts-Einstellung in PlayerContext gespeichert ist, wird auch eine Komponente, die lediglich das Qualitäts-Symbol anzeigt, bei jedem Update neu gerendert.

    Die Reihenfolge der Provider wird zu einem impliziten Vertrag

    WatchlistProvider benötigt die ID des angemeldeten Benutzers von AuthProvider, weshalb es innerhalb dieses Providers platziert werden muss. In der JSX wird diese Abhängigkeit nicht explizit dargestellt, und eine Umordnung der Provider während einer Refaktorierung kann die Anwendung auf schwer nachvollziehbare Weise beschädigen.

    Ein realistisches Symptom: Ein Team kombiniert einen Spielerkontext mit einem Benachrichtigungskontext, und die Profilierung zeigt, dass Fortschrittsaktualisierungen zu erneuten Darstellungen der Benachrichtigungen führen. Nichts scheint sichtbar kaputt zu sein, doch der React Profiler zeigt weitaus mehr Darstellungen, als die Benutzeroberfläche benötigt.

    Der Kontext kann angepasst werden – beispielsweise indem schnell ändernde Werte in eigenen Kontexten abgelegt oder Provider-Werte gememorisert werden – doch jede Workaround-Lösung führt zu mehr Providern und zusätzlicher Komplexität. In solchen Fällen ist oft ein spezieller Store einfacher.

    Stufe drei: Redux Toolkit Slices

    Viele Teams greifen in dieser Phase zu leichteren Stores wie Zustand. Redux Toolkit bleibt eine gute Wahl, wenn man mehrere miteinander verbundene State-Slices hat, Zeitreisendebugging wünscht und eine einzige, vorhersehbare Quelle der Wahrheit schätzt – außerdem beseitigt es den größten Teil des Overheads, der klassisches Redux so umständlich machte.

    Jede Sorge wird zu einem Slice. Der Player-Slice enthält die aktuelle Episode, den Fortschritt sowie die Qualität und definiert Reducer zur Änderung der Episode sowie zum Aktualisieren des Fortschritts. Beachten Sie, dass die Reducer den state direkt verändern scheinen; Redux Toolkit verwendet im Hintergrund Immer, sodass diese Zuweisungen sicher neue, unveränderliche Zustände erzeugen:

    // 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;
        },
      },
    });
    

    Die Verkettung der Komponenten ist verschwunden. Ein einziger <Provider store={store}> umhüllt die Anwendung, und die Komponenten lesen nur das, was sie benötigen, mithilfe von useSelector. Da useSelector den ausgewählten Wert bei jeder Neuansicht vergleicht, wird PlayButton, das die Abonnementstufe auswählt, nur dann erneut angezeigt, wenn sich die Stufe ändert – und nicht jedes Mal, wenn der Fortschrittsschritt aktualisiert wird. Dieses selektive Abonnementmodell beseitigt den größten Teil der unnötigen Neuansichten, die die Context-Version erzeugte. Bei Selectoren ist dennoch Vorsicht geboten: Das Zurückgeben eines neu erstellten Objekts oder Arrays aus einem Selector untergräbt den Vergleich und führt wieder zu zusätzlichen Neuansichten.

    Ein weiterer großer Vorteil sind die Redux DevTools. Wenn man Schritt für Schritt jede gesendete Aktion durchgeht – wie zum Beispiel „Play“ gedrückt, der Fortschritt aktualisiert oder die Episode gewechselt wird – und genau sieht, wie sich der Zustand entwickelt hat, sind Fehler bei der Wiedergabe viel leichter zu diagnostizieren als durch das Nachverfolgen von Werten über eine Pyramide aus Providern.

    Phase vier: RTK Query für den Serverzustand

    Ein Großteil der Komplexität der Anwendung betrifft überhaupt nicht den UI-Zustand. Es handelt sich dabei um Serverzustand: das Katalog, die Suchergebnisse, die Episodendetails sowie die Wunschliste, die auf der Backend-Seite gespeichert sind. Der traditionelle Ansatz kombiniert useEffect mit mehreren useState-Aufrufen pro Anfrage, um Daten, Ladezustände und Fehler manuell zu verfolgen – was oft zu Rennbedingungen und doppelten Abfragen führt.

    RTK Query ersetzt das durch einen API-Slice. Die untenstehende Definition legt eine Basis-URL fest und deklariert zwei Abfrageendpunkte – einen für Anime, die nach Genre gefiltert werden, und einen für Episodendetails:

    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 erzeugt für jeden Endpunkt einen entsprechend benannten React-Hook, den man aus dem Slice exportiert:

    export const { useGetAnimeListQuery, useGetEpisodeDetailsQuery } = catalogApi;
    

    Ein Komponente erhält somit die Daten sowie den Lade- und Fehlerzustand in einer einzigen Zeile:

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

    Es gibt weder manuell geschriebene Effect-Funktionen noch ein manuelles Ladeflag. Die herausragende Eigenschaft ist das Caching. Wenn ein Benutzer die Kategorie „Action“ öffnet, weg navigiert und zurückkehrt, wird die im Cache gespeicherte Liste sofort angezeigt, während RTK Query im Hintergrund neu abruft, falls die Daten veraltet sind. Identische Anfragen von mehreren Komponenten teilen sich einen einzigen Netzwerkaufruf.

    In der Zeile für das Fortsetzen des Anschauens sorgt die auf Tags basierende Invalidierung dafür, dass die Benutzeroberfläche aktuell bleibt: Abfragen deklarieren mit providesTags, was sie liefern, und Mutationen deklarieren mit invalidatesTags, was sie ändern. Wenn eine Mutation zur Aktualisierung des Fortschritts ausgeführt wird, lädt RTK Query die betroffenen Abfragen automatisch neu, sodass keine Komponente manuell einen Neuladen auslösen muss. Für Tags ist ein Eintrag tagTypes im API-Slice sowie ein Mutation-Endpunkt erforderlich – beides ist im obigen Auszug nicht enthalten; unser Leitfaden zu dem Senden von Daten mit RTK Query-Mutationen zeigt diesen Teil. Denken Sie auch daran, dass der Reducer und das Middleware des API-Slices dem Store hinzugefügt werden müssen, damit das Caching funktioniert.

    Jede Art von Zustand einem Tool zuordnen

    Bei einer Anwendung wie dieser sieht eine sinnvolle Aufteilung so aus:

    • Lokaler Zustand mit useState für alles, was niemals den Komponenten verlässt: Formulareingaben, Schalter sowie Zustände bei Überfahren oder Öffnen.
    • Context API für einfache, selten verändernde globale Werte wie Theme oder Sprache. Sie ist bei vielen Kontexten oder häufig aktualisierten Werten ungeeignet.
    • Redux Toolkit für komplexe, miteinander verbundene Client-Zustände wie Authentifizierung, Abonnementstufe, Player-Zustand und Wunschliste, die von vielen unabhängigen Komponenten gelesen und geschrieben werden.
    • RTK Query für alles, was vom Backend stammt – dadurch werden ganze Gruppen von Fehlern beseitigt: veraltete Daten, Konkurrenzen bei Anfragen sowie überflüssige Abrufe.

    Der häufige Fehler besteht darin, dies als eine Art „Alles-oder-Nichts“-Entscheidung zu betrachten – entweder alles in Context oder alles in Redux. Diese Werkzeuge lösen unterschiedliche Probleme, und eine ausgereifte Anwendung kombiniert sie in der Regel.

    Wichtige Erkenntnisse

    • Prop-Drilling ist ein Signal, zunächst die Struktur zu überarbeiten; greifen Sie nur auf den globalen Zustand zurück, wenn die Daten tatsächlich über entfernte Teile des Datenbaums geteilt werden müssen.
    • Context lädt bei jeder Änderung alle Nutzerkomponenten neu, was es zu einem ungeeigneten Ort für häufig wechselnde Werte wie den Wiedergabefortschritt macht.
    • Versteckte Abhängigkeiten in der Reihenfolge der Provider stellen ein Wartungsrisiko dar, das mit jedem neuen Context zunimmt.
    • Die auf Selektoren basierenden Abonnements von Redux Toolkit sowie die DevTools machen den gemeinsamen Client-Zustand sowohl schneller zugänglich als auch einfacher zu debuggen.
  • Halten Sie den Serverzustand außerhalb der selbst implementierten Effekte: Das Caching sowie die Invalidierung von Tags bei RTK Query kümmern sich automatisch um die Aktualität, sofern der Store und die Tags korrekt konfiguriert sind.