Урок 1: Зачем нужны менеджеры состояний? Проблемы подъема состояния в React

Это первый урок из нашего большого курса по 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, которому нужно знать о бюджете, чтобы попросить денег на новый велосипед.

jsx
// Главный компонент, хранящий состояние
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:

  1. Сложность рефакторинга. Если вам нужно изменить структуру компонентов (например, убрать Children и рендерить Son напрямую в Family), вам придется переписывать все промежуточные звенья.

  2. Неявность зависимостей. Становится сложно отследить, откуда и куда передаются данные. Компонент Son зависит от состояния в Family, но связь эта не очевидна.

  3. Ненужные ре-рендеры. Когда состояние 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.

  1. Предсказуемость. Redux основан на трех простых и неизменных принципах (единый источник истины, состояние только для чтения, изменения через чистые функции). Это делает поток данных в вашем приложении абсолютно предсказуемым и легким для понимания.

  2. Отладка. Инструменты разработчика Redux DevTools. Они позволяют вам отслеживать каждое действие, «путешествовать во времени» по состоянию приложения и легко находить баги.

  3. Сообщество и экосистема. Огромное сообщество, множество готовых middleware, библиотек и обучающих материалов.

  4. Redux Toolkit (RTK). Раньше главной претензией к Redux был большой объем шаблонного кода. Redux Toolkit это официальный, рекомендуемый набор инструментов, который решает эту проблему. Он включает в себя утилиты, которые упрощают настройку хранилища, создание редьюсеров и действий и настоятельно рекомендуется к использованию.

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

Практическая задача

Чтобы окончательно закрепить понимание проблемы prop drilling, давайте усложним наш пример с семьей.

Задача:

  1. Создайте состояние isFatherInGoodMood (в хорошем ли настроении папа) в компоненте Family.

  2. Компонент Parents должен иметь кнопку, которая меняет это состояние (папа пришел с работы и его настроение изменилось).

  3. Компонент Son должен знать о настроении папы. Если папа в хорошем настроении, сын может попросить денег. Если нет, токнопка должна быть заблокирована, а текст на ней должен меняться на «Папа не в духе, лучше не беспокоить».

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

Пример того, как может выглядеть Family после изменений:

jsx
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». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий