Inicio / Artículos / Capas de estado en una aplicación de streaming con React: Context, Redux Toolkit y RTK Query

Capas de estado en una aplicación de streaming con React: Context, Redux Toolkit y RTK Query

Sigue un frontend de transmisión de videos desde el uso de prop drilling, pasando por proveedores anidados, hasta las slices de Redux Toolkit y RTK Query, y aprende qué tipo de estado pertenece a cada herramienta.

1694 palabras

Casi todas las aplicaciones React en desarrollo llegan a un punto en el que el componente raíz queda oculto bajo una pila de proveedores, y algún temporizador de reproducción hace que la campana de notificaciones se vuelva a renderizar. La solución rara vez es una sola biblioteca; consiste en reconocer que diferentes tipos de estado necesitan ubicaciones distintas. Esta guía utiliza una interfaz frontal para transmisión de video como estudio de caso, pasando por el uso de propiedades directas, la API Context y Redux Toolkit con RTK Query, para finalizar con una regla práctica para elegir entre ellas.

El ejemplo: una interfaz frontal de streaming

Imagínese las características principales de un servicio de streaming de anime o videos:

  • inicio de sesión y niveles de suscripción, gratuitos o premium
  • una lista de reproducción y una fila de “continuar viendo”
  • estado del reproductor: episodio actual, progreso de reproducción y configuración de calidad
  • navegación por el catálogo e búsqueda con filtros de género
  • notificaciones de nuevos episodios y recordatorios de suscripción
  • Cada uno de estos elementos debe encontrarse en algún lugar del árbol de componentes, y varios componentes que están separados entre sí necesitan leerlos. Es precisamente en esta combinación donde las decisiones iniciales sobre el estado rinden frutos o se convierten en refactorizaciones lentas y costosas meses después.

    Etapa uno: propagación de propiedades

    El primer instinto es elevar el estado al ancestro común más cercano y pasarlo hacia abajo. Para aplicaciones pequeñas esa es la decisión correcta. Sin embargo, en una interfaz de streaming, el camino desde la raíz hasta un botón puede ser largo:

    App → MainLayout → ContentSection → AnimeGrid → AnimeCard → PlayButton

    Supongamos que PlayButton debe conocer el nivel de suscripción para decidir si mostrar un ícono de bloqueo premium. Ese nivel tiene que ser transmitido a través de MainLayout, ContentSection y AnimeGrid, ninguno de los cuales lo utiliza. Ellos existen en esa cadena únicamente como intermediarios.

    Un prop perforado es tolerable. Los problemas comienzan cuando un segundo valor no relacionado, como la lista de seguimiento, tiene que seguir el mismo camino. Cada componente intermedio ahora contiene props que no comprende, lo que dificulta su reutilización en otros lugares, su prueba de forma aislada y su comprensión. Antes de recurrir a una biblioteca, vale la pena leer por qué el uso de prop drilling por sí solo no es motivo para instalar Redux o Zustand; la composición suele acortar estas cadenas. Sin embargo, en esta aplicación los datos son realmente globales.

    Segunda etapa: Contexto y la pirámide de proveedores

    La API Context de React es el siguiente paso lógico. Se crea un AuthContext, se envuelve el árbol en un AuthProvider, y PlayButton lee ese estado directamente con useContext(AuthContext). De esta manera desaparece la necesidad de profundizar en los niveles del árbol.

    Luego aparecen las demás preocupaciones globales, cada una con su propio proveedor, y la raíz del árbol comienza a verse así:

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

    Esto es lo que los desarrolladores denominan “infierno de proveedores”: la raíz de la aplicación se convierte en un conjunto de envoltorios anidados, cada uno añadiendo una capa de indirección. El ruido visual es el menor de los problemas.

    El depuración requiere un mapa

    Cuando la reproducción funciona mal, primero hay que saber qué proveedor controla ese estado y luego seguir el anidamiento para encontrar dónde se produce el cambio. El árbol en sí mismo no proporciona esa información.

    Cada actualización llega a todos los consumidores

    Cuando cambia el valor de un contexto, se vuelve a renderizar todos los componentes que consumen ese contexto, incluso aquellos que solo utilizan una pequeña parte de él. Para continuar viendo el contenido es necesario guardar playbackProgress cada pocos segundos. Si ese valor se encuentra en PlayerContext junto con la configuración de calidad, un componente que solo muestra el indicador de calidad sigue volviéndose a renderizar en cada actualización.

    El orden de los proveedores se convierte en un contrato implícito

    WatchlistProvider necesita el ID del usuario conectado proveniente de AuthProvider, por lo que debe estar anidado dentro de este. Nada en el JSX hace explícita esa dependencia, y reordenar los proveedores durante una refactorización puede dañar la aplicación de maneras difíciles de rastrear.

    Un síntoma realista: un equipo combina un contexto de jugador con un contexto de notificación, y el perfilador revela que las actualizaciones de progreso están provocando la regeneración de notificaciones. No hay nada visible que esté roto, pero el React Profiler muestra muchas más generaciones de elementos de lo necesario para la interfaz.

    Se puede ajustar el contexto, por ejemplo dividiendo los valores que cambian con frecuencia en su propio contexto o memorizando los valores del proveedor, pero cada solución temporal añade más proveedores y más complejidad. En ese punto, a menudo es más sencillo utilizar un almacén dedicado.

    Etapa tres: slices de Redux Toolkit

    Hoy en día, muchos equipos recurren a almacenes más ligeros como Zustand en esta etapa. Redux Toolkit sigue siendo una opción sólida cuando se tienen varios slices de estado relacionados, se desea depurar con funcionalidades de “viaje en el tiempo” y se valora tener una única fuente de verdad predecible; además, elimina la mayor parte del código genérico que hacía que el Redux clásico fuera problemático.

    Cada preocupación se convierte en una porción. La porción del jugador que se muestra a continuación contiene el episodio actual, el progreso y la calidad, y define reducidos para cambiar el episodio y actualizar el progreso. Obsérvese que los reducidos parecen mutar directamente state; Redux Toolkit utiliza Immer en el fondo, por lo que estas asignaciones generan un nuevo estado inmutable de forma segura:

    // 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 anidación ya no existe. Un único <Provider store={store}> envuelve la aplicación, y los componentes solo leen lo que necesitan mediante useSelector. Dado que useSelector compara el valor seleccionado entre renders, PlayButton, al elegir un nivel de suscripción, vuelve a renderizarse solo cuando ese nivel cambia, y no cada vez que avanza el progreso. Este modelo de suscripción selectiva elimina la mayor parte de los renders innecesarios que producía la versión basada en Context.

    Otra gran ventaja son las Redux DevTools. Al seguir paso a paso cada acción enviada, como “play pressed”, “progress updated” o “episode changed”, y ver exactamente cómo evoluciona el estado, resulta mucho más fácil diagnosticar los errores de reproducción que tratar de rastrear los valores a través de una pirámide de proveedores.

    Etapa cuatro: RTK Query para el estado del servidor

    Gran parte de la complejidad de la aplicación no se debe al estado de la interfaz de usuario, sino al estado del servidor: el catálogo, los resultados de búsqueda, los detalles de los episodios y la lista de reproducción almacenados en el backend. El enfoque tradicional combina useEffect con varias llamadas a useState por solicitud para registrar manualmente los datos, el estado de carga y los errores, lo que tiende a generar condiciones de carrera y solicitudes duplicadas.

    RTK Query reemplaza eso con una API slice. La definición a continuación establece una URL base y declara dos puntos finales de consulta: uno para los anime filtrados por género y otro para los detalles de los episodios:

    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 genera un hook de React para cada punto final, nombrado en función de él, el cual se exporta desde la slice:

    export const { useGetAnimeListQuery, useGetEpisodeDetailsQuery } = catalogApi;
    

    Luego, un componente obtiene los datos, así como el estado de carga y de error, en una sola línea:

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

    No existe ningún efecto escrito a mano ni ninguna bandera de carga manual. La característica destacada es el caché. Si un usuario abre la sección de géneros, navega y vuelve, la lista en caché aparece inmediatamente, mientras que RTK Query vuelve a obtener los datos en segundo plano si estos están desactualizados. Las solicitudes idénticas desde varios componentes comparten una sola llamada a la red.

    En la fila de continuación de la reproducción, la invalidación basada en etiquetas mantiene la interfaz de usuario precisa: las consultas declaran qué datos proporcionan mediante providesTags, y las mutaciones declaran qué cambios realizan con invalidatesTags. Cuando se ejecuta una mutación de actualización de progreso, RTK Query vuelve a cargar automáticamente las consultas afectadas, por lo que ningún componente necesita activar una recarga manualmente. Las etiquetas requieren un elemento tagTypes en el segmento de API y un punto de extremidad para mutaciones, ninguno de los cuales aparece en el fragmento anterior; nuestra guía sobre enviar datos con mutaciones de RTK Query explica esa parte. Recuerde también que el reductor y el middleware del segmento de API deben añadirse al almacén para que el caché funcione.

    Asociar cada tipo de estado a una herramienta

    Para una aplicación como esta, una división razonable sería la siguiente:

    • Estado local con useState para todo aquello que nunca sale del componente: campos de formulario, controles de activación/desactivación, estados de hover y abierto.
    • Context API para valores globales simples que cambian con poca frecuencia, como el tema o el idioma. Presenta dificultades cuando hay muchos contextos o valores que se actualizan con frecuencia.
    • Redux Toolkit para estados del cliente complejos e interconectados, como la autenticación, el nivel de suscripción, el estado del reproductor y la lista de seguimiento, que son leídos y escritos por muchos componentes no relacionados.
    • RTK Query para todo lo que proviene del backend, eliminando toda una categoría de errores: datos obsoletos, competencias en las solicitudes y búsquedas redundantes.

    El error común es tratar esto como una elección de tipo todo o nada, poniendo todo en Context o todo en Redux. Estas herramientas resuelven problemas diferentes, y una aplicación madura suele combinarlas.

    Puntos clave

    • El uso de Prop drilling es una señal para reestructurar primero; recurrir al estado global solo cuando los datos se comparten realmente entre partes alejadas del árbol.
    • Context vuelve a renderizar cada consumidor con cada cambio, lo que lo convierte en un lugar inadecuado para valores de alta frecuencia como el progreso de reproducción.
    • Las dependencias ocultas en el orden entre proveedores representan un riesgo de mantenimiento que aumenta con cada nuevo contexto.
    • Las suscripciones basadas en selectores de Redux Toolkit y las herramientas DevTools hacen que el estado del cliente compartido sea más rápido y fácil de depurar.
  • Mantenga el estado del servidor al margen de los efectos implementados manualmente: el caché y la invalidación de etiquetas de RTK Query se encargan de garantizar la actualidad por usted, siempre y cuando el almacén y las etiquetas estén configurados correctamente.
  • Lecturas relacionadas