Современное приложение выбирает… Redux, Context или Recoil?

Поскольку веяния в управлении глобальным состоянием постоянно меняются, то выбор в пользу того или иного варианта может оказаться затруднительным. Долгое время таким предпочтительным вариантом была Redux. Однако после того, как Context API с возможностью управления состоянием стал частью React, многие поспешили объявить о преждевременной кончине Redux. И вот теперь с появлением новоиспеченного проекта Recoil, разработанного Facebook, в нашем распоряжении оказывается библиотека для React, которая с легкостью интегрируется с ее новейшими функциональностями. 

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

Перейдя по ссылке https://github.com/jogilvyt/redux-context-recoil, вы сможете ознакомиться с итоговым вариантом кода (каждая интеграция представлена в отдельной ветке). 

Приложение 

Я создал простое приложение-планировщик, отображающее список элементов с возможностью их удаления и форму для добавления новых. Оно вызывает фиктивный API с setTimeout для получения результатов, создания элемента и удаления его из списка:

Базовое приложение-планировщик

Для получения базовой меры производительности я начал вовсе не с управления глобальным состоянием. При загрузке приложения выполнялись 3 процесса отрисовки: начальная отрисовка экрана загрузки заняла 5.3 мс, ана отрисовкупосле загрузки всех элементов ушло 7 мс

Отрисовка после получения результатов 

Поскольку это простое приложение, то любые отличия в производительности будут незначительными, чего нельзя сказать о работе с более сложным вариантом. Тем не менее данный эксперимент поможет составить общее представление о влиянии каждой из библиотек на производительность. 

Информация к размышлению 

Начиная работу над новым проектом, всегда следует удостовериться в выборе правильных инструментов, поэтому сейчас самое время спросить себя, нужно ли вам вообще управление глобальным состоянием. Множество простых приложений будут вполне работоспособными и более удобными для обслуживания без дополнительных затрат ресурсов, связанных со сторонней библиотекой или даже Context API.

Будучи UI-инженерами, нам свойственно с легкомысленным энтузиазмом хвататься за каждый новенький инструмент. Не стоит попадаться в эту ловушку  —  подумайте, нужна ли вам система управления глобальным состоянием, прежде чем вкладывать силы в ее интеграцию.

Redux

Redux  —  наиболее проверенный временем инструмент управления глобальным состоянием. Он не зависит от фреймворка, чем объясняется его востребованность в мире фронтенд-разработки. Кроме того, какое-то время эта библиотека была единственным средством выхода за пределы локального состояния. Однако в связи с недавним добавлением Context API и появлением нового конкурента в лице Recoil сможет ли Redux отстоять свои позиции в современном приложении React?

Производительность 

Прежде всего, проясним вопрос производительности. При начальной загрузке происходит всего 2 этапа отрисовки против 3-х в базовом случае. Однако общая продолжительность немного увеличивается  —  6 мс уходит на отрисовку состояния загрузки и 10 мс  —  на список после получения элементов.

Redux: отрисовка после получения результатов 

После добавления нового элемента отрисовка заняла 2.9 мс, а после его удаления  —  2 мс. В целом, производительность довольно высокая за исключением изначальных затрат ресурсов при загрузке приложения.

Удобство для разработчика

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

С другой стороны, Redux обеспечивает наилучшие практики для разработки. Она предоставляет простой шаблон, который избавляет от необходимости продумывать большое количество решений по реализации функциональности. Мне также нравится тот факт, что Redux позволяет абстрагировать API-запросы в действия, сосредотачивая управление в одном месте. И вместо того, чтобы ворошить компоненты в попытке определить, какой из них делает запрос, намного удобнее сразу знать, куда перейти для обновления API-вызова. 

Удобство для пользователя

Управление состояниями загрузки и ошибок с помощью Redux требует пользовательской реализации. Лично я отслеживаю в редукторе глобальную переменную isLoading и затем управляю состоянием загрузки для добавления или удаления элементов в каждом компоненте. Это самый обычный прием, который несложно реализовать. Но, забегая вперед,  —  в этой области никто не сравнится с Recoil. 

Context API

В версии 16.3 к React был добавлен новый Context API, обеспечивающий обмен состоянием между компонентами без необходимости “поднимать” его к общему родителю. В настоящее время его широко используют как способ хранения глобального состояния, особенно для более статических данных, таких как информация о пользователе или настройки темы. Сравним его характеристики производительности с Redux. 

Производительность 

Считается, что Context не отличается высокой производительностью, поскольку при обновлении состояния в Provider каждый компонент, использующий этот инструмент, будет повторно отображаться, даже если соответствующее ему состояние не изменилось. 

В нашем приложении при загрузке происходило три этапа отрисовки: начальная заняла 5.9 мс, 2.9 мс ушло на отрисовку после получения данных и 7.9 мс  —  на отображение элементов списка дел. 

Context: отрисовка после получения результатов 

По большей части полученные результаты схожи с итогами эксперимента в Redux. Однако при добавлении или удалении элемента временные показатели выше: отрисовка после добавления элемента  —  4.9 мс, а после удаления  —  4.5 мс, тогда как в Redux соответственно  —  2.9 мс и 2 мс.

Удобство для разработчика 

В этом отношении Context API действительно выигрышный. Он встроен в React, поэтому его можно постепенно вводить в проект, если потребуется управление глобальным состоянием, без каких-то кардинальных изменений архитектуры или добавления новой библиотеки. А как же легко его интегрировать с хуками! В данном приложении был создан хук useToDos, который извлекает состояние из контекста и затем может быть вызван в любом задействующем его компоненте:  

const { toDos } = useToDos();

Его можно даже интегрировать с хуком React useReducer для управления сложным состоянием. На мой взгляд, это эффективный способ управления общим состоянием компонента, имеющего много подкомпонентов и сложную функциональность. 

Удобство для пользователя 

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

Recoil

Recoil  —  новая библиотека, представление которой в свое время вызвало немало шума. Она продвигается Facebook, команда которой также поддерживает React. Recoil отлично интегрируется со многими последними возможностями React, такими как многопоточный режим. Библиотека находится на ранних стадиях разработки и пока не рекомендована к применению в производственном приложении. Но даже с учетом текущего этапа развития какие же результаты мы получим при сравнении ее с предыдущими вариантами? 

Производительность 

У Recoil начальная загрузка оказалась самой медленной из трех вариантов. Всего выполнялось 5 процессов отрисовки: первый занял 11.2 мс; время двух последующих немного ускорилось  —  1.1 мс и 0.2 мс; на отрисовку после загрузки списка дел ушло 5 мс; и наконец, итоговая отрисовка элементов на экране составила 1.9 мс

Recoil: Отрисовка одного только экрана загрузки заняла 11.2 мс

Однако добавление и удаление элемента было выполнено намного быстрее: отрисовка в первом случае  —  2.4 мс, а во втором  —  3.7 мс. В целом эти показатели сопоставимы с Redux. 

При этом стоит отметить, что размер пакета Recoil составляет 45.1 К (в соответствии с моим плагином VS Code Import Cost), тогда как размер Redux  — 11.7 К (при объединении пакетов Redux и React-Redux), а вот Context, являясь частью React, собственного размера не имеет. И хотя эти цифры могут изменяться по ходу работы с библиотекой, об этом соотношении следует помнить. 

Удобство для разработчика 

Несмотря на то, что я в первый раз соприкоснулся с Recoil, работать с ней оказалось легко после того, как я разобрался в атомах и селекторах. Особенно я оценил возможность делать асинхронные запросы в селекторах для инициализации состояния из API-запроса: 

export const toDosQuery = selector({
  key: "ToDosQuery",
  get: async () => await getToDos(),
});

Recoil чрезвычайно упрощает использование состояния в компонентах, принцип работы которого аналогичен useState:

const [toDos, setToDos] = useRecoilState(toDosAtom);

Небольшие сложности возникли при обновлении состояния после добавления или удаления элемента. Я подумал, что при наличии такого асинхронного селектора было бы здорово иметь аналогичную функцию-сеттер для обновления состояния на основе ответа API-запроса. Позднее следует рассмотреть добавление этой возможности, а пока я просто сделал API-запрос в компоненте и установил состояние ответа: 

import { useState } from "react";
import { useRecoilState } from "recoil";

import { toDosAtom } from "../../recoil";
import { addToDo } from "../../api";

const Form = () => {
  const [inputValue, setInputValue] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [, setTodos] = useRecoilState(toDosAtom);

  const handleInputChange = e => {
    setInputValue(e.target.value);
  };

  const onSubmit = async e => {
    e.preventDefault();
    setIsLoading(true);
    const res = await addToDo(inputValue);
    setTodos(res);
    setInputValue("");
    setIsLoading(false);
  };

  return (
    <form className="form" onSubmit={onSubmit}>
      <input
        type="text"
        placeholder="Add a to do"
        className="input"
        value={inputValue}
        onChange={handleInputChange}
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading}>
        Submit
      </button>
    </form>
  );
};

Удобство для пользователя 

На мой взгляд, с этой стороны Recoil раскрывается во всей свой красе. Она интегрируется с многопоточным режимом React, позволяя с помощью Suspense обрабатывать состояние загрузки, а с ErrorBoundary —  состояние ошибок. Это означает, что вы можете создавать приложение намного более декларативным способом. Мне удалось обернуть приложение в компонент Suspense с резервной копией, и теперь он естественным образом отображает экран загрузки до момента завершения API-запроса и загрузки списка дел. 

Этот подход сопровождается одним побочным эффектом  —  выбор некоторых архитектурных решений остается за разработчиком. Интеграция Recoil в зрелое приложение может вызвать затруднения. Кроме того, если вы привыкли делать все по-своему, то вам будет непросто работать в рамках ограничений этой библиотеки. Однако она определенно больше подходит для React, чем Redux. 

Заключение 

Какое-то время Redux занимала лидерские позиции в управлении глобальным состоянием, и в ближайшем будущем она их за собой, несомненно, сохранит. Однако Context и Recoil начинают серьезную борьбу за первенство, поскольку с React они совмещаются гораздо эффективнее.

Я бы порекомендовал Context для небольших приложений, где непринципиально незначительное снижение производительности. Что касается более крупных проектов, то, пока Recoil находится еще на стадиях разработки, ее не стоит применять в производственных приложениях. Но со временем она наверняка предоставит большее удобство как для разработчика, так и для пользователя. Одно удовольствие видеть, как легко Recoil интегрируется с новыми функциональностями React. Как только мы начнем использовать ее в приложениях, она станет самым предпочтительным вариантом для управления глобальным состоянием. 

Благодарю за внимание! Надеюсь, что материал данной статьи помог вам определиться с выбором оптимальной для вас библиотеки управления глобальным состоянием. 

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Jack Taylor: Redux, Context, or Recoil: Which One Is Best for Your Modern Web App?

Предыдущая статьяТри способа захвата скриншотов с помощью Selenium WebDriver
Следующая статьяКак улучшить работу с кодом на TypeScript с VSCode