Используйте React Context для общей среды, а не как хранилище мусора.
Контекст распределяет значения по дереву; он не определяет принадлежность, не обновляет границы и не использует модель состояния. Лучше использовать узкие поставщики и реальные инструменты для управления состоянием сервера.
Источники: Most Developers Use React Context for the Wrong Problems
Передача пропсов не всегда является архитектурной ошибкой
React Context часто используется в качестве быстрого решения. Значение передается через три компонента, которые им никогда не пользуются. Другому экрану нужен текущий пользователь. Модальное окно должно открываться из глубины иерархии. Постоянная передача пропсов становится утомительной, поэтому появляется провайдер, и структура вдруг кажется более упорядоченной.
Промежуточные компоненты отбрасывают ненужные пропсы. Глубоко расположенные дочерние компоненты получают необходимые данные через хуки. Такая структура кажется более осмысленной, чем постоянная передача одних и тех же аргументов на каждом уровне.
Эта внешняя выгода может заслонить более важный вопрос: действительно ли это значение должно находиться в Context?
Контекст решает одну проблему: родитель может задать значение для потомков, не проходя через каждый промежуточный файл. Однако он не определяет, где должно храниться состояние, как часто должны происходить обновления, следует ли связывать несвязанные значения вместе или какой компонент управляет рабочим процессом.
Эти решения остаются прежними. Контекст может скрыть их, поскольку доступ к данным становится невидимым. Потомок может читать данные, не указывая их в свойствах. Поставщик может собирать несвязанные поля, поскольку всё кажется «глобальным». Небольшое обновление может распространиться среди десятков потребителей, использующих один объект контекста. Видимо, переиспользуемый компонент может зависеть от поставщика, существующего только на одной странице.
Слабость заключается не в API, а в использовании упрощённого доступа вместо чёткого определения ответственности.
Пропсы имеют плохую репутацию из-за заметной повторяемости. Значение начинается с высокого уровня, проходит через компоненты интерфейса и в конце достигает того элемента, который его нуждается. Средние слои напоминают сеть коммуникаций, поэтому Context кажется улучшением.
Иногда такая повторяемость представляет собой полезную информацию. Она показывает источник, от какой ветви зависит значение, и насколько далеко простирается эта зависимость. Пропсы позволяют искать введенные данные. Когда значение меняется или исчезает, его влияние остается локальным и очевидным.
Context устраняет такую структуру. Потомок может начать обработку данных без необходимости обновления промежуточных компонентов. Дерево по-прежнему зависит от источника данных, но эта связь больше не отображается на интерфейсе компонента.
Это не означает, что каждая длинная цепочка пропсов должна оставаться. Когда крупная поддерево действительно имеет одну общую проблему, использование множества связей становится избыточным. Ошибка заключается в том, что каждое повторяющееся пропс считается доказательством отсутствия контекста.
Иногда значение оказывается слишком высоким из-за неправильного определения владельца. Страница хранит черновик диалога и передает его через несколько уровней в форму. Контекст мог бы облегчить доступ, но более совершенный дизайн позволяет форме или диалогу самим управлять черновиком.
Иногда композиция устраняет необходимость промежуточного этапа. Родительский элемент может создать элемент, нуждающийся в данных, и передать его как children в макет. Макет никогда не узнаёт о данной зависимости.
Повторяющиеся пропсы — это лишь симптом. Контекст — это всего лишь одно из решений, и он не является стандартным способом диагностики.
Контекст не гарантирует безопасного преобразования локального состояния в глобальное
Часто встречающейся практикой является создание специального контекста там, где несколько компонентов должны обновлять одно и то же состояние. Провайдер хранит значения, предоставляет методы для их изменения, а каждый его потомок может читать или записывать эти значения.
Обычно всё начинается просто:
const AppContext = createContext(null);
function AppProvider({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeModal, setActiveModal] = useState(null);
const [selectedRecord, setSelectedRecord] = useState(null);
return (
<AppContext.Provider
value={{
sidebarOpen,
setSidebarOpen,
activeModal,
setActiveModal,
selectedRecord,
setSelectedRecord,
}}
>
{children}
</AppContext.Provider>
);
}
Доступ к данным сосредоточен в одном месте, но смысл состояния по-прежнему остаётся неясным. Видимость боковой панели, открытие модального окна и выбор конкретной записи часто относятся к разным функциям и реализуются в разное время.
Затем любой потомок в иерархии может напрямую переписать механизм хранения данных провайдера. Строка в таблице может установить выбор, открыть модальное окно или закрыть боковую панель. Компоненты не выражают своих намерений; они просто обращаются к общим полям и формируют любую необходимую комбинацию данных.
Следует невидимое связывание компонентов. Модальное окно предполагает, что выбор уже сделан. Таблица исходит из того, что закрытие модального окна сбрасывает выбор. Панель бокового вида реагирует на рабочий процесс, который ей не принадлежит. Эти взаимосвязи не отражены ни в редьюсере, ни в операциях, ни в границах функциональности — они существуют лишь потому, что многие компоненты могут вызывать различные функции установки значений.
Перемещение полей в контекст лишь изменяет место их хранения. Четкость смысла не возникает автоматически.
Более надежный поставщик функций предоставляет операции вместо примитивных функций установки значений, и только для одной целостной задачи. Система диалогов может предлагать функции openDialog и closeDialog. Поставщик функций аутентификации может предоставлять данные сессии и операции входа. Поставщик тем может предоставлять текущую тему и специальную функцию для её обновления.
Не ограничивайтесь мыслью о том, что «нескольким компонентам нужен доступ». Спросите себя, имеют ли они общий жизненный цикл и одну и ту же задачу.
Один объект контекста может создать огромную площадь для обновлений
Контекст также отвечает за распространение обновлений. Когда меняется значение поставщика, потребители этого контекста могут перерисовать содержимое. Это нормально, если это значение связано с одной конкретной задачей и потребители действительно зависят от его изменений.
Проблемы возникают, когда один поставщик объединяет значения с разными частотами обновлений.
function ApplicationProvider({ children }) {
const value = {
user,
theme,
locale,
notifications,
sidebarOpen,
featureFlags,
liveJobStatus,
};
return (
<ApplicationContext.Provider value={value}>
{children}
</ApplicationContext.Provider>
);
}
Переключатель темы не должен делать недействительными таблицы, которым требовалась лишь выбранная запись. Флаг открытия боковой панели не должен обновлять формы, которым нужен был только идентификатор пользователя. Объединение этих полей в один объект делает каждого потребителя чувствительным ко всем записям, если только вы тщательно не сохраните информацию в кэше и не разделите контексты.
Команды иногда замечают такие проблемы только после анализа производительности. К тому времени многие функции уже используют этот сложный механизм. Чтобы решить проблему, необходимо найти каждого потребителя и определить, какую именно часть информации ему действительно требовалась.
Лучше использовать узкие по функционалу провайдеры, соответствующие темпам обновлений. Храните компоненты с высокой частотой изменений в одном месте, а данные сессий — в другом. Сохраняйте значения провайдеров в кэше, если идентичность объекта иначе менялась бы при каждой отрисовке. Оценивайте последствия перед тем, как добавить «ещё одно поле» в существующий набор данных.
Контекст может скрывать отсутствие границ компонентов
Когда подключение определённых функций кажется затруднительным, контекст может скрыть отсутствие чётких границ между компонентами. Редактор продукта, который бесшумно зачитывает информацию о действующем продукте, черновиках, процессах верификации, правах доступа и статусе изменений из контекста страницы, может быть понятен только внутри этой конкретной страницы.
Некоторые зависимости естественным образом являются неявными, поскольку они описывают среду выполнения, а не данные функций — темы, локализацию, маршрутизацию, общий HTTP-клиент. Такие зависимости обычно имеют широкий охват и относительную стабильность.
Данные функций отличаются. Их скрытие в Context устраняет избыточные параметры, но увеличивает архитектурную зависимость. Отсутствие параметров не означает отсутствия зависимостей; часто это означает, что зависимости просто стали невидимыми.
Спросите себя, можно ли понять и протестировать компонент вне провайдера, который его в настоящее время окружает. Если ответ «нет», возможно, Context компенсирует отсутствие чёткого контракта между параметрами или композицией.
Context часто используется вместо надлежащего управления состоянием на сервере
Ещё одно неправильное использование — загружение полученных данных в Context только потому, что их нужно нескольким компонентам. Провайдер запрашивает данные один раз, сохраняет ответ и предоставляет доступ к данным, процессу загрузки, ошибкам и возможности обновления. Это выглядит как общий слой данных без необходимости в дополнительных библиотеках.
Для реального серверного состояния быстро требуется гораздо больше: удаление дублирующихся запросов, правила обработки устаревших данных во время переопределения, попытки повторного выполнения операций, возможность отмены, пагинация, обновления после изменений и сохранение предыдущих данных при обновлении.
Контекст не предоставляет ничего из этого. Он лишь передаёт то, что было вычислено поставщиком.
Поставщик постепенно превращается в ручной кэш. Компоненты зависят от его особенностей обновления. Изменения в данных ручным способом вносятся в общие массивы. Разные поставщики создают немного отличающиеся способы загрузки и обработки ошибок. Отключение определённого маршрута может привести к потере кэша; использование двух поставщиков может привести к двойной загрузке данных.
Существуют специализированные библиотеки данных, потому что у удалённого состояния есть свой жизненный цикл, отличный от локального состояния интерфейса: владение данными на сервере, свежесть данных, кэширование, аннулирование кэша, конкурентность и способы обработки сбоев.
Контекст может предоставлять настроенный клиент или стабильную зависимость функций. Редко это подходящее место для воссоздания координации запросов с помощью обычных useState и useEffect. Сфокусированный провайдер, координирующий серверные данные в рамках одного рабочего процесса, всё ещё может быть эффективным, но делание данных доступными в глобальном масштабе не решает проблему синхронизации.
Обеспечение доступности данных не является тем же самым, что и управление удалёнными данными.
Контекст — не замена модели состояния
Когда рабочий процесс становится сложнее, команды иногда помещают его в контекст, чтобы каждый шаг мог видеть одни и те же поля. Процессы оплаты, адаптации пользователей, редактирования и многоэтапные формы часто оказываются в таком положении.
Это может сработать, когда провайдер представляет один рабочий процесс и обеспечивает контролируемые переходы. Однако всё становится хрупким, когда контекст представляет собой лишь набор полей и функций для их изменения.
Провайдер оплаты может содержать поля с адресом, информацией о платеже, вариантами доставки, текущим этапом обработки, ошибками проверки, флагами загрузки, флагами модальных окон, итоговой суммой и множеством функций для установки значений. Каждый компонент может независимо изменять любую часть этих данных.
Общее использование хранилища не является тем же, что и определение правил. Изменение страны может потребовать сброса настроек доставки; изменение способа доставки может потребовать пересчёта итоговой суммы. Возврат на предыдущий этап может сохранить некоторые поля и сделать другие недействительными. Ошибка при оплате может изменить ход обработки, не удаляя информацию об адресе.
Для описания таких взаимосвязей необходима модель состояния — редюсеры или явные операции, которые кодируют допустимые переходы между состояниями. Контекст может распространять эту модель, но сам не является ею.
Без правил возникают невозможные комбинации: шаг оплаты без действительной доставки, одновременное существование состояний «успех» и «ошибка», варианты доставки, относящиеся к предыдущему адресу. Вам не обязательно нужна формальная библиотека автоматов состояний. Тем не менее необходимо четко определить, какие факты существуют самостоятельно, какие комбинации допустимы, и какие события изменяют несколько полей при одном переходе.
Контекст позволяет создать хорошую модель. Он не может спасти запутанную модель, просто переместив её на более высокий уровень в иерархии.
Глобальный доступ: может ли он ослабить чувство собственности?
Значения в контексте легко читаются и легко записываются из множества источников. Удобство может подорвать чувство собственности.
Контекст уведомлений обновляется с помощью обработчиков сети, форм, страниц и фоновых задач. Контекст выбора изменяется под влиянием таблиц, диалоговых окон, горячих клавиш и изменений маршрутов. Контекст предпочтений записывается непосредственно каждым элементом управления, отображающим настройку.
Когда множество авторов используют один хранилище, ответственность за корректность становится нечеткой.
Более четкая ответственность возникает при узкоспециализированных операциях. Лучше использовать addNotification, dismissNotification и clearExpiredNotifications, чем примитивный setNotifications. Лучше использовать signIn, signOut и refreshSession, чем универсальный setSession. Операции несут смысл и позволяют поставщику сохранять неизменяемые свойства.
Даже тогда следует сохранять фокус на поставщике. Универсальный диспетчер для каждой функции создает глобальную связность под другим названием.
Многие компоненты могут читать информацию о текущем пользователе. Гораздо меньше из них должны заменять сессию. Контекст остается удобным для обслуживания, когда поставщик контролирует жизненный цикл, а потребители взаимодействуют через простой контракт.
Поставщик контекста может превратиться в скрытый монолит
Компоненты большого размера проявляют себя через длину файлов. Провайдеры большого размера остаются более тихими в течение длительного времени.
Структура может казаться модульной — с маленькими компонентами и хуками, — в то время как один провайдер контролирует состояние приложения, сетевые запросы, сохранение данных, события браузера, разрешения, уведомления, модальные окна и производные значения. Файлы компонентов остаются небольшими, поскольку провайдер берет на себя задачу координации.
Это всё равно монолит; он просто генерирует небольшое количество JSX-кода.
Провайдер может изменяться по независимым причинам. Правила уведомлений, требования темы, изменения сессии и рабочие процессы модальных окон все влияют на один и тот же модуль. Каждый потребитель зависит от его стабильности. Тесты усложняются, поскольку операции взаимодействуют через общее состояние. Производительность снижается, когда обновления распространяются по всей системе. Рефакторинг вынуждает разбираться с предположениями, заложенными в значение, используемое повсюду.
Решение не заключается в бесконечном вложении структур ради самих по себе. Вложение допустимо, когда у каждого поставщика есть четкая ответственность. Несколько сфокусированных поставщиков обычно работают эффективнее, чем один объект, притворяющийся обладающим единым жизненным циклом.
Важна также структура размещения. Контекст, необходимый для одного маршрута, должен охватывать только этот поддерево, а не всё приложение. Узкое размещение сужает как объем зависимостей, так и диапазон обновлений.
Глобальная доступность должна быть достигнута усилиями, а не служить стандартным местом хранения нежелательных элементов.
Контекст отлично подходит для тех проблем, которые он действительно решает
Контекст проявляет свою эффективность тогда, когда поддерево действительно делит общую среду выполнения или стабильные факторы, влияющие на его работу.
Темы подходят, поскольку многим потомкам требуется один и тот же вариант отображения. Локализация удобна, так как форматирование и перевод применяются в широком масштабе. Общий клиент API, среда маршрутизации, диапазон формы или единый рабочий процесс также могут входить в это, когда все потребители находятся внутри одних границ.
Значение не обязательно должно быть статичным. Оно должно иметь целостное значение и шаблон обновления, соответствующий потребителям.
Хорошо спроектированный контекст отвечает на вопросы: в какой среде работает эта поддерево, какие зависимости имеют эти компоненты и к какому конкретному рабочему процессу они относятся?
Плохо спроектированный контекст порождает вопросы: какие значения было сложно передавать в качестве параметров? Этот вопрос приводит к несвязанному состоянию, широким поверхностям обновления, скрытым зависимостям и прямым методам установки значений. Структура взаимодействия исчезает, а принадлежность элементов становится менее очевидной.
Прежде чем добавлять контекст, уточните, существует ли значимая поддерево, делающее общий вывод, совпадает ли у потребителей жизненный цикл, как часто меняется значение, и может ли поставщик опубликовать узкое и стабильное соглашение. Также выясните, какую проблему скроет контекст. Неправильное размещение состояния, отсутствие границ функциональности, слабая композиция, кэширование серверного состояния или неопределенный рабочий процесс не будут устранены простым облегчением доступа.
Контекст должен распределять решения, а не избегать их
Большинство злоупотреблений происходит не из-за непонимания API, а из-за мгновенного облегчения. Цепочка свойств исчезает, компоненты получают доступ, функциональность выпускается без перестройки структуры. Сегодняшний код легче писать.
Проблема возникает, когда объем доступа превышает возможности владения. Не связанные между собой жизненные циклы используют одного поставщика. Потребители перерисовывают содержимое из-за изменений, которые они игнорируют. Компоненты функций зависят от скрытых сред. Поставщики постепенно превращаются в импровизированные хранилища, кэши запросов и движки рабочих процессов, у которых никогда не было четко определенной модели.
Называть Context слабой библиотекой для управления состоянием — значит не понимать сути. Он никогда не претендовал на определение механизмов управления состоянием; он лишь передает значение по иерархии.
Дизайн в целом все равно должен ответить на вопросы: что представляет собой это значение, какой модуль им владеет, кому разрешено его изменять, как часто происходят обновления и какие потребители действительно относятся к нему.
Context работает наилучшим образом, когда эти решения уже ясны.