Почему это антипаттерн?

Автор оригинала: Adam Nathaniel Davis
  • Перевод
Всем привет. В сентябре в OTUS стартует сразу несколько курсов по JS-разработке, а именно: JavaScript Developer. Professional, JavaScript Developer. Basic и React.js Developer. В преддверии старта этих курсов мы подготовили для вас еще один интересный перевод, а также предлагаем записаться на бесплатные демо-уроки по следующим темам:


А теперь перейдём к статье.





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

Как один компонент получает доступ к информации (особенно к переменной состояния), которая находится в другом компоненте? Как один компонент вызывает функцию, которая находится в другом компоненте?

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

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

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

Стандартный подход: используем пропсы для передачи значений


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

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

App→ обращается к →ContentArea
ContentArea→ обращается к →MainContentArea
MainContentArea→ обращается к →MyDashboard
MyDashboard→ обращается к →MyOpenTickets
MyOpenTickets→ обращается к →TicketTable
TicketTable→ обращается к последовательности →TicketRow
Каждый TicketRow→ обращается к →TicketDetail

Теоретически эта гирлянда может накручиваться еще долго. Все компоненты являются частью целого. Если точнее, частью иерархии. Но тут возникает вопрос:

Может ли компонент TicketDetail в примере выше считывать значения состояния, которые хранятся в ContentArea? Или. Может ли компонент TicketDetail вызывать функции, которые находятся в ContentArea?
Ответ на оба вопроса — да. Теоретически все потомки могут знать обо всех переменных, которые хранятся в родительских компонентах. Они также могут вызывать функции предков — но с большой оговоркой. Это возможно, только если такие значения (значения состояния или функции) в явном виде переданы потомкам через пропсы. В противном случае значения состояния или функции компонента не будут доступны его дочернему компоненту.

В небольших приложениях и утилитах это особой роли не играет. Например, если компоненту TicketDetail нужно обратиться к переменным состояния, которые хранятся в TicketRow, достаточно сделать так, чтобы компонент TicketRow → передавал эти значения своему потомку → TicketDetail через один или несколько пропсов. Точно так же дело обстоит в случае, когда компоненту TicketDetail нужно вызвать функцию, которая находится в TicketRow. Компонент TicketRow → передаст эту функцию своему потомку → TicketDetail через проп. Головная боль начинается, когда какому-нибудь компоненту, расположенному далекоооо вниз по дереву, нужно получить доступ к состоянию или функции компонента, расположенного вверху иерархии.

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

ContentAreaMainContentAreaMyDashboardMyOpenTicketsTicketTableTicketRowTicketDetail

То есть для того чтобы передать переменную состояния из ContentArea в TicketDetail, нам нужно проделать огромную работу. Опытные разработчики понимают, что возникает безобразно длинная цепочка передачи значений и функций в виде пропсов через промежуточные уровни компонентов. Решение настолько громоздкое, что из-за него я даже пару раз бросал изучение React.

Чудовище по имени Redux


Я не единственный, кто считает, что передавать через пропсы все значения состояния и все функции, общие для компонентов, очень непрактично. Вряд ли вы найдете хоть сколько-нибудь сложное React-приложение, к которому не прилагался бы инструмент управления состоянием. Таких инструментов не так уж мало. Лично я обожаю MobX. Но, к сожалению, «отраслевым стандартом» считается Redux.

Redux — это детище создателей ядра React. То есть они сначала создали прекрасную библиотеку React. Но сразу же поняли, что с ее средствами управлять состоянием практически невозможно. Если бы они не нашли способа решить присущие этой (во всем остальном замечательной) библиотеке проблемы, многие из нас никогда бы не услышали о React.

Поэтому они придумали Redux.
Если React — это Мона Лиза, то Redux — это пририсованные ей усы. Если вы используете Redux, вам придется написать тонну шаблонного кода почти в каждом файле проекта. Устранение проблем и чтение кода становятся адом. Бизнес-логика выносится куда-то на задворки. В коде — разброд и шатание.

Но если перед разработчиками стоит выбор: React + Redux или React без каких-либо сторонних инструментов управления состоянием, они почти всегда выбирают React + Redux. Поскольку библиотеку Redux разработали авторы ядра React, она по умолчанию считается одобренным решением. А большинство разработчиков предпочитают использовать решения, которые были вот так молчаливо одобрены.

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

Большинство знакомых мне React-разработчиков, которые пытались сопротивляться использованию Redux, в конце концов сдались. (Потому что… сопротивление бесполезно.) Я знаю много людей, которые сразу возненавидели Redux. Но когда перед ними ставили выбор — Redux или «мы найдем другого React-разработчика», — они, закинувшись сомой, соглашались принять Redux как неотъемлемую часть жизни. Это как налоги. Как ректальный осмотр. Как поход к стоматологу.

Новый взгляд на общие значения в React


Я слишком упрям, чтобы так просто сдаться. Взглянув на Redux, я понял, что нужно искать другие решения. Я могу использовать Redux. И я работал в командах, которые пользовались этой библиотекой. В общем, я понимаю, что она делает. Но это не значит, что Redux мне нравится.
Как я уже говорил, если без отдельного инструмента для управления состоянием не обойтись, то MobX примерно… в миллион раз лучше, чем Redux! Но меня мучает более серьезный вопрос. Он касается коллективного разума React-разработчиков:

Почему первым делом мы всегда хватаемся за инструмент управления состоянием?

Когда я только начал разрабатывать на React, я провел не одну ночь в поисках альтернативных решений. И я нашел способ, которым пренебрегают многие React-разработчики, но никто из них не может сказать, почему. Объясню.

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

// components.js
let components = {};
export default components;

И все. Только две короткие строчки кода. Мы создаем пустой объект — старый добрый JS-объект. Экспортируем его по умолчанию с помощью export default.

Теперь давайте посмотрим, как может выглядеть код внутри компонента <ContentArea>:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      components.ContentArea = this;
   }

   consoleLog(value) {
      console.log(value);
   }

   render() {
      return <MainContentArea/>;
   }
}

По большей части он выглядит, как вполне нормальный классовый React-компонент. У нас есть простая функция render(), которая обращается к следующему компоненту вниз по дереву. У нас есть небольшая функция console.log(), которая выводит в консоль результат выполнения кода, и конструктор. Но… в конструкторе есть некоторые нюансы.

В самом начале мы импортировали простой объект components. Затем в конструкторе мы добавили новое свойство к объекту components с именем текущего React-компонента (this).В этом свойстве мы ссылаемся на компонент this. Теперь при каждом обращении к объекту components у нас будет прямой доступ к компоненту <ContentArea>.

Давайте посмотрим, что происходит на нижнем уровне иерархии. Компонент <TicketDetail> может быть таким:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      components.ContentArea.consoleLog('it works');
      return <div>Here are the ticket details.</div>;
   }
}

А происходит вот что. При каждом рендере компонента TicketDetail будет вызываться функция consoleLog(), которая хранится в компоненте ContentArea.

Обратите внимание, что функция consoleLog() не передается по всей иерархии через пропсы. Фактически функция consoleLog() не передается никуда — вообще никуда, — ни в один компонент.

И тем не менее TicketDetail может вызвать функцию consoleLog(), которая хранится в ContentArea, потому что мы выполнили два действия:

  1. Компонент ContentArea при загрузке добавил в общий объект components ссылку на себя.
  2. Компонент TicketDetail при загрузке импортировал общий объект components, то есть у него был прямой доступ к компоненту ContentArea, несмотря на то что свойства ContentArea не передавались компоненту TicketDetail через пропсы.

Этот подход работает не только с функциями/колбэками. Его можно использовать для прямого запроса значений переменных состояния. Представим, что <ContentArea> выглядит так:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   render() {
      return <MainContentArea/>;
   }
}

Тогда мы можем написать <TicketDetail> вот так:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return <div>Here are the ticket details.</div>;
   }
}

Теперь при каждом рендере компонента <TicketDetail> он будет искать значение переменной state.reduxSucks в <ContentArea>. Если переменная вернет значение true, функция console.log() выведет в консоль сообщение. Это произойдет, даже если значение переменной ContentArea.state.reduxSucks никогда не передавалось вниз по дереву — ни одному из компонентов — через пропсы. Таким образом, благодаря одному простому базовому JS-объекту, который «обитает» за пределами стандартного жизненного цикла React, мы можем сделать так, чтобы любой потомок мог считывать переменные состояния непосредственно из любого родительского компонента, загруженного в объект components. Мы даже можем вызывать функции родительского компонента в его потомке.

Возможность вызова функции непосредственно в дочерних компонентах означает, что мы можем изменять состояние родительских компонентов прямо из их потомков. Например так.

Для начала в компоненте <ContentArea> создадим простую функцию, которая меняет значение переменной reduxSucks.

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
   }

   render() {
      return <MainContentArea/>;
   }
}

Затем в компоненте <TicketDetail> мы вызовем этот метод через объект components:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}

Теперь после каждого рендера компонента <TicketDetail> пользователь сможет нажимать кнопку, которая будет изменять (переключать) значение переменной ContentArea.state.reduxSucks в режиме реального времени, даже если функция ContentArea.toggleReduxSucks() никогда не передавалась вниз по дереву через пропсы.

С таким походом родительский компонент может вызвать функцию непосредственно из своего потомка. Вот как это можно сделать. Обновленный компонент <ContentArea> будет выглядеть так:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
   }

   render() {
      return <MainContentArea/>;
   }
}

А теперь добавим логику в компонент <TicketTable>. Вот так:

// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';

export default class TicketTable extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucksHasBeenToggledXTimes: 0 };
      components.TicketTable = this;
   }

   incrementReduxSucksHasBeenToggledXTimes() {
      this.setState((previousState, props) => {
         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
      });      
   }

   render() {
      const {reduxSucksHasBeenToggledXTimes} = this.state;
      return (
         <>
            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
            <TicketRow data={dataForTicket1}/>
            <TicketRow data={dataForTicket2}/>
            <TicketRow data={dataForTicket3}/>
         </>
      );
   }
}

В результате компонент <TicketDetail> не изменился. Он все еще выглядит так:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}

Вы заметили странность, связанную с этими тремя классами? В иерархии нашего приложения ContentArea — это родительский компонент для TicketTable, который является родительским компонентом для TicketDetail. Это означает, что когда мы монтируем компонент ContentArea, он еще «не знает» о существовании TicketTable.А функция toggleReduxSucks(), записанная в ContentArea, неявно вызывает функцию потомка:
incrementReduxSucksHasBeenToggledXTimes().Получается, код работать не будет, так?

А вот и нет.

Смотрите. Мы создали в приложении несколько уровней, и есть только один путь вызова функции toggleReduxSucks(). Вот так.

  1. Монтируем и рендерим ContentArea.
  2. В ходе этого процесса в объект components загружается ссылка на ContentArea.
  3. В результате монтируется и рендерится TicketTable.
  4. В ходе этого процесса в объект components загружается ссылка на TicketTable.
  5. В результате монтируется и рендерится TicketDetail.
  6. У пользователя появляется кнопка «Изменить значение reduxSucks» (Toggle reduxSucks).
  7. Пользователь нажимает кнопку «Изменить значение reduxSucks».
  8. Нажатие кнопки вызывает функцию toggleReduxSucks(), которая записана в компоненте ContentArea.
  9. Это в свою очередь вызывает функцию incrementReduxSucksHasBeenToggledXTimes() из компонента TicketTable .
  10. Все работает, потому что к тому моменту, когда пользователь сможет нажать кнопку «Изменить значение reduxSucks», ссылка на компонент TicketTable будет загружена в объект components. А функция toggleReduxSucks() при вызове из ContentArea сможет найти ссылку на функцию incrementReduxSucksHasBeenToggledXTimes(), записанную в TicketTable, в объекте components.

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

Инструменты управления состоянием — на свалку


Как я уже объяснил, я глубоко уверен в том, что Redux не идет ни в какое сравнение с MobX. И когда мне выпадает честь работать над проектом с нуля (к сожалению, нечасто), я всегда агитирую за MobX. Не за Redux. Но когда я разрабатываю собственные приложения, я вообще редко использую сторонние инструменты управления состоянием — практически никогда. Вместо этого я просто-напросто кеширую объекты/компоненты, когда это возможно. А если это подход не работает, я частенько возвращаюсь к решению, которое используется в React по умолчанию, то есть просто передаю функции/переменные состояния через пропсы.

Известные «проблемы» этого подхода


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

  • Лучше всего он работает с одиночками.

    Например, в нашей иерархии в компоненте <TicketTable> находятся компоненты <TicketRow> со связью «ноль-ко-многим». Если вы захотите кешировать ссылку на каждый потенциальный компонент внутри компонентов <TicketRow> (и их дочерних компонентов <TicketDetail>) в кеш components, вам придется сохранить их в массив, и тут могут возникнуть сложности. Я всегда избегал этого.
  • При кешировании объекта components предполагается, что мы не можем использовать переменные/функции из других компонентов, если они не были загружены в объект components. Это очевидно.
    Если архитектура вашего приложения делает такой подход нецелесообразным, то не надо его использовать. Он идеально подходит для одностраничных приложений, когда мы уверены в том, что родительский компонент всегда монтируется раньше потомка. Если вы решили сослаться на переменные/функции потомка непосредственно из родительского компонента, создавайте такую структуру, которая будет выполнять эту последовательность только после загрузки потомка в кеш components.
  • Можно считывать переменные состояния из других компонентов, ссылки на которые хранятся в кеше components, но если вы захотите обновить такие переменные (через setState()), вам придется вызвать функцию setState(), которая записана в соответствующем компоненте.

Ограничение ответственности


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

Хм… Не делай этого. Они морщатся и ведут себя так, будто я только что испортил воздух. Что-то в моем подходе кажется им… неправильным. И при этом еще никто не объяснил мне, исходя из своего богатого практического опыта, что именно не так. Просто все считают мой подход… кощунством.

Поэтому даже если вам нравится этот подход или если он кажется вам удобным в некоторых ситуациях, я не рекомендую говорить о нем на собеседовании, если вы хотите получить должность React-разработчика. Думаю, что, даже просто разговаривая с другими React-разработчиками, нужно миллион раз подумать, прежде чем сказать об этом способе, а может, лучше вообще ничего не говорить.

Я обнаружил, что JS-разработчики — и, в частности, React-разработчики — бывают слишком категоричны. Иногда они действительно объясняют, почему подход А «неправильный», а подход Б «правильный». Но в большинстве случаев они просто смотрят на фрагмент кода и объявляют, что он «плохой», — даже если сами не могут объяснить, почему.

Так почему же этот подход так раздражает React-разработчиков?


Как я уже говорил, ни один из моих коллег не смог обоснованно ответить, чем плох мой способ. А если кто-нибудь и готов удостоить меня ответом, то обычно это одна из следующих отговорок (их мало).

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

    Окей...Понял. Но те самые люди, которые с ходу отвергли этот подход, с удовольствием будут использовать Redux (или MobX, или любое другое средство управления состоянием) почти со всеми классами/функциями в своих React-приложениях. Я не отрицаю, что иногда без инструментов управления состоянием действительно сложно обойтись. Но любой такой инструмент по своему характеру — это гигантский генератор зависимостей. Каждый раз, когда вы используете инструменты управления состоянием с функциями/классами, вы захламляете приложение зависимостями. Обратите внимание: я не говорил, что нужно отправлять каждую функцию или класс в кеш объекта components. Вы самостоятельно решаете, какие именно функции/классы будут кешироваться в components, а какие функции/классы будут обращаться к тому, что вы поместили в кеш components. Если вы пишете чистую вспомогательную функцию/класс, то наверняка моя идея с кешем components вам не подходит, потому что для кеширования в components компоненты должны знать о других компонентах приложения. Если вы пишете компонент, который будет использоваться в разных фрагментах кода вашего приложения или даже в разных приложениях, не применяйте этот подход. Но опять же, если вы создаете такой глобальный компонент, в нем не нужно использовать ни Redux, ни MobX, ни какое-либо еще средство управления состоянием.
  • Просто в React «так не делается». Или… Это не соответствует отраслевым стандартам.

    Ага… Это мне говорили не раз. И знаете что? Когда я это слышу, я даже немножко перестаю уважать своего собеседника. Если единственная причина — это какое-то размытое «так не делается» или «отраслевой стандарт», который сегодня один, а завтра другой, то разработчик просто чертов лентяй. Когда появилась React, у нас не было вообще никаких инструментов управления состоянием. Но люди начали изучать эту библиотеку и решили, что они нужны. И их создали.Если вы действительно хотите соответствовать «отраслевым стандартам», просто передавайте все переменные состояния и все обратные вызовы функций через пропсы.Но если вам кажется, что базовая реализация React не удовлетворяет ваши потребности на 100 %, откройте глаза (и разум) и взгляните повнимательней на нестандартные решения, которые не были одобрены лично господином Дэном Абрамовым.

Итак, что скажете ВЫ?


Я написал этот пост, потому что уже годами использую этот подход (в личных проектах). И он работает превосходно. Но каждый раз, когда я вылезаю из своего личного «пузыря» и пытаюсь вести интеллектуальную беседу об этом подходе с другими, сторонними React-разработчиками, я сталкиваюсь только с категоричными заявлениями и бестолковыми суждениями об «отраслевых стандартах».

Этот подход действительно плох? Ну правда. Я хочу знать. Если это действительно «антипаттерн», я буду безмерно благодарен тем, кто обоснует его неправильность. Ответ «я к такому не привык» меня не устроит. Нет, я не зациклился на этом методе. Я не утверждаю, что это панацея для React-разработчиков. И я признаю, что он подходит не для всех ситуаций. Но может хоть кто-нибудь объяснить мне, что в нем не так?

Мне очень хочется узнать ваше мнение по этому поводу — даже если вы разнесете меня в пух и прах.

Бесплатные уроки:


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 40

    +3
    Redux — это детище создателей ядра React.

    Ден Абрамов написал Redux ещё до присоединения к команде Реакта, если не ошибаюсь (по крайней мере, он не был сотрудником FB)
    Лично я обожаю MobX. Но, к сожалению, «отраслевым стандартом» считается Redux.
    MobX потихоньку тоже становится стандартом.
      0
      подтверждаю, последние два года попадается только Mobx слава богу)
        0
        Хотелось бы, чтобы MobX был стандартном.
        Но я не встречал ни одного готового шаблона админки или фреймворка с Mobx(
        Если какие-то готовые компоненты используют менеджер состояний, то это почти всегда redux(
        Еще recoil появился. Из-за того, что он от разрабов фейсбука, у него большой шанс стать стандартом в будущем.
        Плюс в современных примерах а-ля «есть context и хуки, наконец-то можем проще, без redux» по-прежнему по привычке тянут эти редьюсеры, диспетчеры. Эх, испортил Ден фронтенд основательно и надолго(
        +1
        Самый большой фейл в экосистеме React'a — это связка с Redux и ему подобным хламом.

        Самый большой фейл многих разработчиков которые пишут на React'e — не знание того, что есть MobX и то, что Redux давно должен лежать на помойке.

        Связка React + MobX делает из React'a отдельную и главное реактивную технологию, уже совершенно взрослую и реально мощную. Без боли, без страданий и без тонн лютого и не поддерживаемого говнокода (который считается нормой в react+redux апликухах).
          +2

          Мне кажется, что автор Redux пытался изобразить https://www.martinfowler.com/eaaDev/EventSourcing.html на джаваскрипте. Получилось10 строчек кода и тут он обрёл просветление. Я не вижу в этом ни чего плохого, кроме попытки оставить комьюнити один на один с низкоуровневым API на несколько лет. А потом они родили redux-toolkit, и этим стало можно пользоваться без слёз.

            +1
            redux-toolkit, и этим стало можно пользоваться без слёз.

            Все еще со слезами на глазах, но явно лучше, чем ничего =)
            У них добавлены многие штуки из коробки, но все еще не очень понятно, что делать с nested данными, особенно, если у вложенных данных нет уникальных id. А вложенность трехуровневая. (Реальный кейс, где redux-toolkit не справляется без шаманства).


            С Mobx/Mobx-state-tree шаманить практически не нужно.

          +6

          Описанный подход создаёт жёсткую связь от подчинённого компонента к владеющему компоненту. Нормально в обратном направлении — владелец вызывает метод подчинённого, причём для этого не нужен какой-то глобальный объект, ведь владелец знает своих подчинённых, если же подчинённый лежит в каком-то компоненте-обёртке, то на нём (компоненте-обёртке) создаётся проксирующий метод. В описанном же варианте периодически будут возникать ситуации, когда казалось бы универсальный компонент перемещается в другое место с полным удалением бывшего владельца и от этого ломается. Правильный подход — использование всплывающих событий — подчинённый компонент просто эмиттит событие о том, что с ним случилось, если какому-то предку вверх по иерархии нужно как-то на это реагировать, он подписывается на это событие. Куда бы не перемещался подчинённый компонент, он не ломается, тк. связь не жёсткая. Если два компонента никак не вложены друг в друга и всё равно должны взаимодействовать, то взаимодействие делается через общего для обоих предка, который подписывается на событие одного компонента и вызывает метод другого.

            +1
            если же подчинённый лежит в каком-то компоненте-обёртке, то на нём (компоненте-обёртке) создаётся проксирующий метод и компонент-владелец использует его
            На фронте вложенность часто бывает глубокой. Проксирование через десятки компонентов вниз по иерархии становится проблемой, от которой и пытаются уйти, используя для взаимодействия какую-нибудь штуку снаружи всей этой иерархии, расстояние до которой не зависит от вложенности.
              +2
              Проксирование через десятки компонентов вниз

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


              UPD: глобальным объектом в этом случае обычно делается корневой компонент приложения который виден из всех компонентов как this.ownerComponent (или this.rootComponent).

                +1

                В чем преимущество компонента как глобального объекта по сравнению с контекстом?

                  +1

                  Для передачи вниз контекст прекрасно подойдёт, а вот для передачи вверх без создания жёсткой связи он мне кажется малопригоден.


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

                    +1
                    в контекст можно положить функцию обновления состояния, например dispatch из useReducer
                      0

                      Я там уже добавил обновление в свой коммент выше)).

            +2
            А чем вам не подошел контекст? Отлично подходит для использования состояния компонента на любой вложенности в любом из потомков.
              0

              ИМХО меняете шило на мыло как по мне. Как минимум, вы могли бы иметь index.js файл для компонентов, в котором бы экспортился законнекченный к стору компонент, и соответственно когда вам нужна связь с хранилищем вы юзаете его, когда нет — используете напрямую Component.js. Своим подходом вы создаёте жёсткую связь (что является антипаттерном в программировании в принципе, вспоминаем high cohesion и low coupling) между компонентами.
              Ну и плюс в вашем подходе нужно разбираться, у него могут быть свои подводные камни, а с redux уже всё всем понятно и всё известно. Так что как по мне, не стоит изобретать велосипед в очередной раз.

                +2

                Если так не хочется брать "такой большой" mobx, то лучше уж взять подход отсюда — https://habr.com/ru/post/491684/ — чем делать жёсткую циклическую связь сразу между всеми компонентами.

                  –1
                  Redux позволяет писать действительно большие проекты, MobX — быстрее стартануть. Чем он лучше — сказать не могу, мне нравится насколько понятно происходящее с приложением, когда я смотрю список actions и изменения state в Reactotron (работаю React-Native). Как это было бы в MobX — хз.

                  Чистый redux смысла нет использовать, можно подключить redux-toolkit (или reduxsauce) чтобы не писать много лишнего кода.
                  Для модификаций состояний можно использовать seamless-immutable. Это если не нравиться много писать.

                  В приведенном примере плохо — слишком большая связность между компонентами, и state все равно в компонентах. Я не представляю как бы я поддерживал и рефакторил такую архитектуру где были бы сотни компонентов.
                    0
                    Для модификаций состояний можно использовать seamless-immutable

                    Кстати, redux-toolkit идет в комплекте с immer.js от авторов mobx (совпадение? не думаю)
                    Это позволяет писать простой и понятный код в редьюсерах:


                    const reducers = {
                      resetStatusImmer = state => {
                        state.meta.status = 'initial'
                      },
                    
                      resetStatusCommon = state => {
                        return {
                          ...state,
                          meta: {
                              ...state.meta,
                              status: 'initial'
                          }
                        }
                      },
                    }
                      +2
                      Redux позволяет писать действительно большие проекты, MobX — быстрее стартануть.

                      Мда, куда катится мир. Они оба позволяют писать гигантские проекты. Что за «логика» такая?
                      Если ты не понимаешь что происходит в твоем коде, не умеешь пользоваться console.log, не умеешь пользоваться IDE (find references / find usages и т.п.), то у меня для тебя плохие новости и никакой тут mobx или redux не поможет.
                      –1

                      Во-первых, Ваш подход вынуждает использовать компоненты-классы вместо компонентов-функций. Это может замедлить приложение, когда оно разрастётся, не говоря о том, что их сложнее дебажить.
                      Во-вторых, асинхронность. Если TicketDetails получает данные через HTTP, и если таких компонентов много (а их будет много), каждому придется добавлять какой-нибудь EventEmitter или Promise для синхронизации — а это дополнительный boilerplate-код и нарушение принципа single responsibility.
                      Ну и в-третьих, добавление жёстких зависимостей усложняет написание юнит-тестов, добавляя больше boilerplate-кода ещё и в них.

                        +2
                        Во-первых, Ваш подход вынуждает использовать компоненты-классы вместо компонентов-функций. Это может замедлить приложение, когда оно разрастётся, не говоря о том, что их сложнее дебажить.

                        О, вы хотите об этом поговорить? Вот есть классы, которые, как мы все понимаем, после транспиляции превратятся в лежащую где-то коллекцию функций (статика) и коллекцию функций в «особой» группе prototype (не статика). А теперь, давайте с этого момента поподробнее — как именно это может замедлить приложение, когда оно разрастётся?
                          –3
                          Коллекция функций потребует больше ресурсов, чем одна функция.
                            +2

                            Чем одна функция, у которой внутри — та же самая коллекция, только в виде анонимных замыканий? Ну-ну.

                              +1
                              Спойлер:
                              Вы никогда не увидите разницу глазами в производительности между классовыми компонентами реакта и функциональными в реальных приложения.
                              Выигрыш одной из сторон на пару процентов вообще никакой погоды никому не делает.
                              А вот удобство разработки делает огромную погоду.
                          0
                          Переходите на vue, там можно сделать такую штуку, как реактивные контейнеры состояния (естественно, не только со свойствами, но и с методами), которые можно использовать напрямую или наследовать от них свои вычисляемые свойства. А в Vue 3 этот подход еще более расширен благодаря прямому доступу к системе реактивности.
                            0
                            В MobX подобное тоже есть.
                            0
                            Интересно, а кто будет подчищать «старье» в этом components.js? Там буду висеть ссылки на компоненты, которые уже давно не нужны. Как-то этот момент не освещен.
                              +1
                              Вы кажется в той же ловушке, что и большинство react-разработчиков. В ловушке под названием react.
                              Я не к тому, что реактом пользоваться не надо, нет, прекрасная штука — нежно люблю и сам использую каждый день. Нет, я к тому, что вы разрабатываете на реакте, завязываясь на сам реакт. Ну и порождаете жесткую связанность(coupling), когда она вовсе не нужна.

                              Почему-то DDD пока мало проникает во фронтэнд, и мало кто отделяет доменные вещи от средства отображения. Все данные, всю логику, вообще почти все можно безболезненно отделить от реакта и это будет настоящим работающим ядром приложения без всяких отображений. Можно даже наверно сказать — это будет «реализацией стейт-менеджера», который по сути «переменная + сеттер + observable». Каждый может это сделать, а если проникнуться DDD — можно сделать это очень хорошо.
                                +1
                                Ну не знаю, я например пишу на реакте с 2015 года, но с 2016 года использую реакт(по прямому назначению) только как view слой и не более, состояние и бизнес логика описана в MobX'e и в обычных функциях и классах хелперах.
                                Так что не все находятся в ловушке, но многие, к великому сожалению.
                                  +2
                                  Почему-то DDD пока мало проникает во фронтэнд, и мало кто отделяет доменные вещи от средства отображения.

                                  Это не «почему-то», а как раз потому, что модные-молодежные фреймворки прошлого десятилетия (текущие топ-3 как раз оттуда) неявно способствовали отказу от MVC (и отказу от моделирования данных по DDD, соответственно) и пропагандировали идеи в духе «ты просто фигачь свой фронт целиком на нашем инструменте, а мы как-нибудь подумаем, чтоб тебе было норм». Разумеется, вторая часть в реальности для реакта никогда не воплощалась: реакт так и остался в первую очередь инструментом контроля за доступом к DOM (для чего он собственно и разрабатывался — чтоб парням в фейсбуке стало понятно, какой код и когда у них меняет DOM). Да и у других тоже не всё шоколадно — тот же ангуляр тащит свой корявенький DI (корявенький — потому что делался очень давно и не меняется из-за соображений обратной совместимости, хотя и сейчас можно сделать значительно более цивилизованно) ради архитектуры высокого уровня, но в вопросах моделирования данных просто говорит «вот у нас тут RxJS есть, уже впиленный в ангуляр — так что вы просто пользуйтесь им».

                                  Текущее поколение «топовых» фронтовых фреймворков пытается подменять собой все части MVC, несмотря на то, что по сей день они в основном лишь отрабатывают букву V, а в части поддержки M и C у них у всех всё крайне небезоблачно.
                                    0
                                    Отличное дополнение, спасибо!
                                      +1
                                      У Vue и Svelte в плане M и C все гораздо лучше конечно из коробки. Только вот они с Tyescript'ом не сильно дружат из коробки в отличии от реакта.
                                        0
                                        Svelte, грят, уже дружит — надо как-нибудь пощупать самому, как будет время.
                                          0
                                          Так и vuejs дружит, но не без нюансов.
                                            0
                                            В том то и дело что и там и там есть нюансы, но в целом по состоянию на сегодняшний день терпимо, но все же хотелось бы лучше
                                              +1
                                              В тот день, когда нам перестанет хотеться лучше — пора отправляться на кладбище.
                                    0

                                    НЛО прилетело и удалило эту запись, опс..

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

                                      Что если components.ContentArea случайно изменится каким-нибудь другим компонентом?
                                      Что если компонент более верхнего уровня будет размонтирован, а компоненты более низкого уровня — нет?
                                      Кроме того, сложнее становится тестировать компоненты независимо, ведь кроме передачи пропсов нужно сформировать подходящий объект components с необходимыми заглушками, ценность этих тестов также падает.
                                      Наконец, хранение ссылок на компоненты в глобальном объекте components, особенно массива ссылок (хоть автор и не рекомендует так делать), приведет к утечкам памяти, поэтому если уж и пользоваться таким подходом, то очень аккуратно, не забываю про очистку ссылок при размонтировании компонентов.

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

                                      Я думаю, что многие разработчики «морщатся» потому, что на интуитивном уровне понимают, что ваш подход противоречит принципам функционального программирования, в духе которого реакт и написан. И тут уже дискуссия перетекает на высокие материи =))

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

                                      Самое читаемое