Это первый урок из нашего большого курса по Redux и Redux Toolkit и сегодня мы разберемся с основные вопросы, зачем вообще нужны эти менеджеры состояний? Почему нельзя обойтись встроенными в React инструментами? Мы с вами на практике увидим проблему, которая возникает в больших приложениях и поймем, как ее решают глобальные менеджеры состояния, такие как Redux. У меня также есть уроки по React.
Давайте начнем с самого начала. В React мы привыкли управлять состоянием компонентов с помощью хука useState. Это прекрасно работает для изолированных компонентов, таких как открытие/закрытие модального окна, значение в поле ввода или состояние кнопки. Локальное состояние это как личный блокнот, все ваши заметки находятся в одном месте и вам удобно с ними работать.
Но представьте, что ваше приложение растет. Из простого одностраничного блога оно превращается в сложный проект с десятками компонентов, множеством страниц и сложной логикой взаимодействия между ними. Например, вам нужно отображать данные пользователя (его имя, аватар) в хедере, в сайдбаре и на странице профиля одновременно. Где хранить эту информацию?
Первое, что приходит в голову, это поднять состояние. Вы помещаете состояние user в ближайшего общего родительского компонента, например в App.js. И вот здесь начинается самое интересное, а точнее сложное.
Чтобы передать данные о пользователе в компонент Header, вы пробрасываете пропс user из App в Header. Это просто. Но что, если компонент Sidebar находится глубоко внутри иерархии, скажем, App -> Layout -> Main -> Sidebar? Вам придется передавать пропс user через каждый из этих промежуточных компонентов, даже если они сами по себе не используют эти данные! Они выступают в роли простых «курьеров», которые передают пропс дальше, вглубь дерева компонентов. Этот феномен и получил меткое название «prop drilling» (прокидка пропсов).
Представьте, что вы руководитель и вам нужно передать указание сотруднику из другого отдела. Вы передаете его своему заместителю, тот начальнику отдела, начальник отдела старшему сотруднику и только потом указание доходит до нужного человека. Каждое промежуточное звено знает об этом указании, но не использует его для своей работы. А теперь представьте, что таких указаний десятки. Система становится громоздкой и неэффективной.
Пример с семейным бюджетом
Давайте напишем небольшое приложение, которое демонстрирует эту проблему. Представьте компонент Family, который хранит состояние семейного бюджета. Внутри него есть компоненты Parents и Children. А внутри Children есть компонент Son, которому нужно знать о бюджете, чтобы попросить денег на новый велосипед.
// Главный компонент, хранящий состояние function Family() { const [budget, setBudget] = useState(1000); // Семейный бюджет return ( <div> <h1>Семейный бюджет: {budget} руб.</h1> <Parents budget={budget} /> <Children budget={budget} /> </div> ); } // Компонент Parents, который использует бюджет function Parents({ budget }) { return ( <div> <h2>Родители</h2> <p>Мы планируем покупки, зная что бюджет: {budget} руб.</p> </div> ); } // Компонент Children, который НЕ использует бюджет, но вынужден его передавать function Children({ budget }) { return ( <div> <h2>Дети</h2> <Son budget={budget} /> </div> ); } // Компонент Son, который наконец-то использует бюджет function Son({ budget }) { const askForMoney = () => { alert(`Можно я возьму 500 руб. из наших ${budget}?`); }; return ( <div> <h3>Сын</h3> <button onClick={askForMoney}>Попросить денег на велосипед</button> </div> ); }
Обратите внимание на компонент Children. Он сам не использует пропс budget, но он вынужден его принимать и передавать дальше, компоненту Son. В нашем маленьком примере это выглядит не страшно. Но в реальном приложении, где таких пропсов может быть 10-15, а цепочка компонентов 5-7 уровней, код превращается в кошмар.
Проблемы prop drilling:
-
Сложность рефакторинга. Если вам нужно изменить структуру компонентов (например, убрать
Childrenи рендеритьSonнапрямую вFamily), вам придется переписывать все промежуточные звенья. -
Неявность зависимостей. Становится сложно отследить, откуда и куда передаются данные. Компонент
Sonзависит от состояния вFamily, но связь эта не очевидна. -
Ненужные ре-рендеры. Когда состояние
budgetменяется вFamily, React перерисовывает не толькоParentsиSon, которые действительно используютbudget, но и компонентChildren, который лишь передает пропс дальше! В маленьких приложениях это незаметно, но в больших может стать «бутылочным горлышком» для производительности.
Понятие «глобального» состояния
Итак, мы поняли проблему. Нам нужно каким-то образом сделать определенные данные доступными для любого компонента в дереве, независимо от его глубины, без необходимости прокидывать их через всех промежуточных «родителей». Нам нужно состояние, которое существует как бы «над» всем приложением, это глобальное состояние.
Возвращаясь к нашей аналогии с офисом, это похоже на общую доску объявлений или корпоративную базу данных. Любой сотрудник (компонент) может подойти к этой доске (подписаться на состояние), прочитать информацию и оставить свои изменения (отправить действие для обновления состояния). При этом ему не нужно получать информацию через всех начальников.
Глобальное состояние это не какая-то супер переменная, доступная отовсюду. Это данные, которые хранятся в специальном объекте вне React-компонентов, но с которыми компоненты могут взаимодействовать по четко определенным правилам.
Другие решения (Context API, MobX, Zustand)
Прежде чем мы будем изучать Redux, важно знать, что он не единственный игрок на поле. React и сообщество предлагают несколько решений этой проблемы.
Context API (встроен в React)
Это первый инструмент, о котором думают React-разработчики. Context API позволяет передавать данные через дерево компонентов без необходимости передавать пропсы на каждом уровне. Вы создаете «контекст» (например, UserContext или BudgetContext), оборачиваете часть приложения в Context.Provider и передаете значение. Затем любой дочерний компонент может получить это значение с помощью хука useContext(UserContext).
-
Плюсы. Встроен в React, простота для малых объемов данных.
-
Минусы. Не предназначен для часто изменяющихся данных. При обновлении контекста перерисовываются все компоненты, которые его используют, что может быть избыточно. Также он не предоставляет готовой архитектуры для сложных обновлений состояния и вся логика обновления обычно лежит в одном большом объекте состояния и функции-редьюсере (useReducer).
MobX
MobX предлагает совершенно другой, объектно-ориентированный подход. Вы создаете классы с наблюдаемыми полями и компоненты автоматически реагируют на изменения этих полей.
-
Плюсы. Очень мало шаблонного кода, высокая производительность, интуитивно понятен для тех, кто любит ООП.
-
Минусы. Подход отличается от «иммутабельного» стиля React.
Zustand
Zustand это очень легковесная и простая библиотека. Ее API можно изучить за 5 минут. Вы создаете хранилище с помощью функции, определяете в нем состояние и функции для его обновления. Любой компонент может подписаться на это хранилище.
-
Плюсы. Минимум шаблонного кода, простота, отличная производительность.
-
Минусы. В очень больших и сложных приложениях может не хватать жесткой архитектуры и встроенных лучших практик, которые есть в Redux.
Почему мы выбираем Redux и Redux Toolkit?
Несмотря на появление многих достойных альтернатив, Redux остается одним из самых популярных, надежных и востребованных менеджеров состояний в экосистеме React.
-
Предсказуемость. Redux основан на трех простых и неизменных принципах (единый источник истины, состояние только для чтения, изменения через чистые функции). Это делает поток данных в вашем приложении абсолютно предсказуемым и легким для понимания.
-
Отладка. Инструменты разработчика Redux DevTools. Они позволяют вам отслеживать каждое действие, «путешествовать во времени» по состоянию приложения и легко находить баги.
-
Сообщество и экосистема. Огромное сообщество, множество готовых middleware, библиотек и обучающих материалов.
-
Redux Toolkit (RTK). Раньше главной претензией к Redux был большой объем шаблонного кода. Redux Toolkit это официальный, рекомендуемый набор инструментов, который решает эту проблему. Он включает в себя утилиты, которые упрощают настройку хранилища, создание редьюсеров и действий и настоятельно рекомендуется к использованию.
Redux это как профессиональный фрезерный станок с ЧПУ. Его нужно настроить и изучить, но затем он позволяет вам создавать сложнейшие изделия с невероятной точностью. Он идеально подходит для больших, сложных приложений, где важны предсказуемость, надежность и возможности отладки.
Практическая задача
Чтобы окончательно закрепить понимание проблемы prop drilling, давайте усложним наш пример с семьей.
Задача:
-
Создайте состояние
isFatherInGoodMood(в хорошем ли настроении папа) в компонентеFamily. -
Компонент
Parentsдолжен иметь кнопку, которая меняет это состояние (папа пришел с работы и его настроение изменилось). -
Компонент
Sonдолжен знать о настроении папы. Если папа в хорошем настроении, сын может попросить денег. Если нет, токнопка должна быть заблокирована, а текст на ней должен меняться на «Папа не в духе, лучше не беспокоить».
Попробуйте реализовать это, продолжая передавать пропсы. Вы увидите, как быстро усложнится код и как много компонентов будут вовлечены в передачу данных, которые им самим не нужны.
Пример того, как может выглядеть Family после изменений:
function Family() { const [budget, setBudget] = useState(1000); const [isFatherInGoodMood, setIsFatherInGoodMood] = useState(true); // Новое состояние const toggleMood = () => setIsFatherInGoodMood(!isFatherInGoodMood); return ( <div> <h1>Семейный бюджет: {budget} руб.</h1> <p>Настроение папы: {isFatherInGoodMood ? 'Хорошее' : 'Плохое'}</p> {/* Передаем и бюджет и состояние настроения и функцию его смены */} <Parents budget={budget} isFatherInGoodMood={isFatherInGoodMood} onMoodToggle={toggleMood} /> {/* Передаем бюджет и состояние настроения вглубь */} <Children budget={budget} isFatherInGoodMood={isFatherInGoodMood} /> </div> ); } // Компонент Children все так же не использует пропсы, но вынужден их передавать! function Children({ budget, isFatherInGoodMood }) { return ( <div> <h2>Дети</h2> <Son budget={budget} isFatherInGoodMood={isFatherInGoodMood} /> </div> ); }
Становится страшновато? Это всего лишь два состояния. В реальном приложении их могут быть десятки. В следующем уроке мы начнем знакомиться с тем, как Redux решает эту проблему, вводя такие понятия, как Store, Actions и Reducers.
Управление состоянием это ключ к созданию масштабируемых и поддерживаемых React-приложений. Понимание проблемы это уже 50% решения. Вы ее теперь понимаете. Дальше будет только интереснее.
Это был первый урок из полного курса с уроками по Redux и Redux Toolkit для начинающих. Переходи по ссылке, чтобы найти все уроки и материалы.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


