Главная / Статьи / Управление слоями в приложении с потоковой передачей данных в React: Context, Redux Toolkit и RTK Query

Управление слоями в приложении с потоковой передачей данных в React: Context, Redux Toolkit и RTK Query

Пройдите обучение по фронтенду для потоковой передачи видео, начиная с использования prop drilling, переходя к вложенным провайдерам, Redux Toolkit slices и RTK Query, и узнайте, какой тип состояния подходит для каждого инструмента.

1694 слов

Почти каждый развивающийся React-приложение достигает момента, когда корневой компонент погружается в стек провайдеров, а таймер воспроизведения каким-то образом повторно отрисовывает иконку уведомлений. Решение редко заключается в использовании одной библиотеки; скорее, речь идет о понимании того, что разные виды состояния требуют разных мест для хранения. В этом обзоре в качестве примера рассматривается фронтенд для стриминга видео: мы рассмотрим подачу данных через параметры компонентов, Context API, Redux Toolkit и RTK Query, а в конце приведем практическое правило для выбора между ними.

Пример: фронтенд для стриминга

Представьте основные функции сервиса стриминга аниме или видео:

  • вход в систему и уровни подписки — бесплатный или премиум
  • список просмотра и строка «продолжить просмотр»
  • состояние плеера: текущая серия, прогресс воспроизведения и настройки качества
  • навигация по каталогу и поиск с фильтрами по жанру
  • уведомления о новых эпизодах и напоминания об оплате подписки
  • Каждый из этих элементов должен находиться где-то в дереве компонентов, и несколько расположенных далеко друг от друга компонентов должны иметь к ним доступ. Именно в такой ситуации ранние решения относительно состояния либо приносят пользу, либо приводят к медленным и дорогостоящим корректировкам спустя месяцы.

    Этап первый: передача данных через пропсы

    Первым побуждением является перемещение состояния к ближайшему общему предку и его передача дальше. Для небольших приложений это правильный подход. Однако в интерфейсе стримингового сервиса путь от корня до кнопки может быть довольно длинным:

    App → MainLayout → ContentSection → AnimeGrid → AnimeCard → PlayButton

    Предположим, что PlayButton должен знать уровень подписки, чтобы решить, следует ли отображать иконку блокировки премиум-версии. Этот уровень должен передаваться через MainLayout, ContentSection и AnimeGrid, но ни один из этих компонентов им не пользуется. Они существуют в этой цепочке лишь в качестве посредников.

    Одна передаваемая через проп свойство ещё терпима. Проблемы начинаются, когда второе, не связанное с ней свойство, такое как список для отслеживания, должно проходить тот же путь. Теперь каждый промежуточный компонент получает свойства, которых он не понимает, что затрудняет их повторное использование в других местах, тестирование изолированно и понимание их функции. Прежде чем обращаться к библиотекам, стоит прочитать почему само использование проп-дриллинга не является основанием для установки Redux или Zustand; композиция часто сокращает такие цепочки. Однако в этом приложении данные действительно являются глобальными.

    Второй этап: контекст и пирамида провайдеров

    API Context в React — это естественный следующий шаг. Вы создаете AuthContext, оборачиваете всю структуру в AuthProvider, и компонент PlayButton получает необходимые данные непосредственно с помощью useContext(AuthContext). Таким образом исчезает необходимость в глубоком проникновении в структуру.

    Затем появляются другие глобальные проблемы, каждая из которых требует собственного провайдера, и структура корня приходит к такому виду:

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

    Это то, что разработчики называют «адом провайдеров»: корень приложения превращается в набор вложенных оберток, каждая из которых добавляет слой косвенного доступа. Визуальный беспорядок — это ещё не самая серьезная проблема.

    Для отладки требуется карта структуры

    Когда процесс воспроизведения работает некорректно, сначала необходимо выяснить, какой именно провайдер управляет данными, а затем проследить вложенность, чтобы найти место их изменения. Сама структура дерева этого не показывает.

    Каждое обновление доходит до всех потребителей

    Изменение значения контекста приводит к повторной отрисовке всех компонентов, которые используют этот контекст, даже тех, что задействуют лишь его часть. Для продолжения отслеживания необходимо сохранять значение playbackProgress каждые несколько секунд. Если это значение хранится в PlayerContext вместе с настройками качества, компонент, который отображает лишь индикатор качества, тем не менее перерисовывается при каждом обновлении.

    Порядок компонентов-поставщиков становится неявным контрактом

    WatchlistProvider нуждается в идентификаторе авторизованного пользователя, получаемом от AuthProvider, поэтому он должен находиться внутри него. В JSX отсутствует явное указание на эту зависимость, и изменение порядка компонентов-поставщиков во время рефакторинга может нарушить работу приложения в трудноотследимых формах.

    Реалистичный симптом: команда объединяет контекст игрока и контекст уведомления, и анализ профиля показывает, что обновления прогресса вызывают повторную отрисовку уведомлений. Визуально ничего не сломано, но React Profiler отображает гораздо большее количество отрисовок, чем требуется интерфейсу.

    Контекст можно настроить, например, разделив часто меняющиеся значения в отдельный контекст или сохраняя значения провайдеров в памяти, но каждое решение приводит к добавлению большего количества провайдеров и сложности в управлении. В таких случаях использование специализированного хранилища данных часто оказывается проще.

    Третий этап: Redux Toolkit slices

    На этом этапе многие команды обращаются к более легким решениям вроде Zustand. Redux Toolkit остается отличным выбором, когда у вас есть несколько связанных фрагментов состояния, требуется отладка с возможностью «путешествия во времени», а также нужен единый предсказуемый источник правды; к тому же он устраняет большую часть шаблонного кода, который делал классический Redux сложным в использовании.

    Каждая проблема превращается в отдельный фрагмент. Фрагмент игрока содержит текущую серию, прогресс и качество, а также определяет функции-редьюсеры для изменения серии и обновления прогресса. Обратите внимание, что эти редьюсеры, кажется, напрямую мутируют state; однако в Redux Toolkit под капотом используется Immer, поэтому такие присваивания безопасно создают новое неизменяемое состояние:

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

    Иерархия компонентов исчезла. Вся приложение оборачивается одним элементом <Provider store={store}>, а компоненты с помощью useSelector читают только то, что им необходимо. Поскольку useSelector сравнивает выбранное значение между перерисовками, компонент PlayButton, отвечающий за выбор уровня подписки, перерисовывается только тогда, когда меняется уровень, а не каждый раз при изменении прогресса. Такая селективная модель подписок устраняет большинство ненужных перерисовок, возникающих в версии с Context.

    Ещё одним значительным преимуществом являются инструменты Redux DevTools. Пошаговый анализ каждого отправленного действия — такого как нажатие кнопки воспроизведения, обновление прогресса или смена эпизода — и возможность точно увидеть, как менялось состояние, значительно облегчают диагностику ошибок воспроизведения по сравнению с отслеживанием значений через иерархию поставщиков данных.

    Четвёртый этап: RTK Query для состояния сервера

    Значительная часть сложности приложения вовсе не связана с состоянием интерфейса. Речь идёт о состоянии сервера: каталоге, результатах поиска, деталях эпизодов и списке для просмотра, хранящихся на сервере. Традиционный подход предполагает использование useEffect в сочетании с несколькими вызовами useState для каждого запроса с целью вручную отслеживания данных, процесса загрузки и ошибок, что часто приводит к конфликтам выполнения и дублированию запросов.

    RTK Query заменяет его фрагментом API. Приведённое ниже определение устанавливает базовый URL и указывает два конца запроса: один для аниме, отфильтрованного по жанру, и другой для деталей эпизода:

    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 генерирует хук React для каждого конца запроса, названный в его честь, который затем экспортируется из фрагмента:

    export const { useGetAnimeListQuery, useGetEpisodeDetailsQuery } = catalogApi;
    

    Затем компонент получает данные, состояние загрузки и ошибок в одной строке:

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

    Не существует ручно написанных функций обработки эффектов и отдельного флага загрузки. Основной особенностью является кэширование. Если пользователь откроет раздел с жанрами, уйдёт и вернётся, кэшированный список отобразится сразу, в то время как RTK Query в фоновом режиме обновит данные, если они устарели. Идентичные запросы от нескольких компонентов используют один сетевой вызов.

    Для режима непрерывного просмотра механизм аннулирования на основе тегов обеспечивает точность интерфейса: запросы указывают, какие данные они предоставляют, с помощью поля providesTags, а мутации — что они изменяют, с помощью поля invalidatesTags. Когда выполняется мутация обновления прогресса, RTK Query автоматически перезагружает соответствующие запросы, поэтому компонентам не нужно вручную запускать перезагрузку. Для тегов требуется запись tagTypes в разделе API slice и конечная точка для мутаций, ни одна из которых не присутствует в приведённом выше фрагменте; наша инструкция по отправке данных с использованием мутаций RTK Query рассматривает этот аспект. Также имейте в виду, что редьюсер и мидлвард API slice должны быть добавлены в хранилище, чтобы кэширование функционировало.

    Сопоставление каждого типа состояния с соответствующим инструментом

    Для подобного приложения разумное разделение выглядит следующим образом:

    • Локальное состояние с использованием useState для всего, что никогда не покидает компонент: полей формы, переключателей, состояний наведения и открытия.
    • Context API для простых глобальных значений, которые редко меняются, таких как тема или язык. Он плохо справляется, когда существует много контекстов или значений, которые обновляются часто.
    • Redux Toolkit для сложного, взаимосвязанного состояния клиента, такого как авторизация, уровень подписки, состояние плеера и список наблюдаемых элементов, с которым работают множество не связанных компонентов.
    • RTK Query для всего, что поступает с сервера, что позволяет устранить целый класс ошибок: устаревшие данные, конкурентные запросы и избыточные загрузки.

    Распространенная ошибка — рассматривать это как выбор «всё или ничего», когда всё помещается в Context или всё — в Redux. Эти инструменты решают разные проблемы, и зрелое приложение обычно сочетает их.

    Основные выводы

    • Использование Prop drilling — это сигнал к первоначальной реструктуризации; обращаться к глобальному состоянию следует только тогда, когда данные действительно нужны в отдаленных частях структуры.
    • Context перерисовывает каждый потребитель при каждой изменении, из-за чего он не подходит для хранения данных с высокой частотой обновления, таких как прогресс воспроизведения.
    • Скрытые зависимости между поставщиками данных представляют риск для технического обслуживания, который увеличивается с каждым новым Context.
    • Система подписок на основе селекторов в Redux Toolkit и инструменты DevTools делают общее состояние клиента более быстрым в использовании и упрощают его отладку.
  • Не допускайте выхода состояния сервера из-под контроля в ручно реализованных эффектах: механизмы кэширования и аннулирования тегов RTK Query самостоятельно обеспечивают актуальность данных, при условии правильной настройки хранилища и тегов.