Как стать автором
Обновить

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

Состояние isOpen нужно в location.query роутера чаще всего (только в виде modal=cart скорее всего).


  • удобнее разработка (100% гарантия, что модалка откроется, даже если hot reload не отработал)
  • всегда можно дать ссылку на модалку пользователю /about?modal=delivery
  • модалки могут открываться через ssr, если там важный для поисковика контент

Редко вижу, чтобы такое делали, а стоило бы.

Вот что только не придумают, лишь бы MobX не использовать где это всё давно уже решено из коробки

Нормальные фреймворки изначально дизайнились «от состояния (иммутабельного и реактивного)» и вот там не приходится потом год за годом добавлять «костыли» для твикинга работы со стейтом тк перформанс не радует. Реакт дизайнился полагая что dirty-checking ихнего vdom c dom это быстро/модно/молодежно и достаточно чтобы отбросить лишние отрисовки. Но с годами до них дошло что dirty-checking сама по себе не быстрая операция и кроме этого еще и vdom висит в памяти что для для мобилок или слабых ПК тоже не очень хорошо. И вот теперь добавляют свои useMemo и прочие хелперы работы со стейтом когда во взрослых фреймворках это изначально сделано. В итоге из изначально простой библиотеки реакт все больше становится костыльной поделкой.
Описанный в статьей метод прост в применении и не требует ничего кроме самого реакта.
Сам по себе голый реакт это фигня, а вот MobX превращает его в отличную вещь. Посмотрите это habr.com/ru/post/485032/#comment_21195466
Я знаю про MobX и да же работаю с ним. Но это не имеет никакого отношения к статье. Одно дело провести рефакторинг приложения опустив нужные стейты по иерархии компонентов, и совсем другое переводить весь проект на новый стейт менеджер.
Перевести на MobX как раз таки не проблема
MobX? Не, не слышал… В очередной раз… ппц…
Не будет ReactRedux перерендеривать SlowComponent при использовании в контейнере только свойств «dog» и «onChange». По крайней мере при использовании connect с дефолтными настройками.
react-redux.js.org/api/connect#pure-boolean
Я прочитал статью, и не понял, при чём тут вообще стейт. Всё, что написано в статье — это не проблема стейта, это проблема компонентного рендера — при рендере некоего компонента его можно отрендерить (при этом произведя все необходимые предварительные движения) только полностью, либо не рендерить совсем.
Итого, всякий раз, когда вы рендерите
    <div>
      <DogName time={time} dog={dog} onChange={setDog} />
      <SlowComponent time={time} onChange={setTime} />
    </div>

вы рендерите это всегда целиком на любое изменение любой зависимости.
Когда же вы рендерите
    <div>
      <DogName time={time} /> // <- убрали передачу состояний
      <SlowComponent time={time} onChange={setTime} />
    </div>

то теперь это будет ре-рендерится целиком только при изменении time.

ЗЫ: Для любителей MobX — я сам очень люблю MobX, но в нём точно так же можно облажаться с рендером по вышеприведенному сценарию. MobX вас точно так же не спасёт от того, что у вас какой-то тяжеловесный компонент стоит рядом с мелким и лёгким в чьём-то общем рендере.
Ну как бы вот тут MobX, банальный пример как именно он должен применяться. codesandbox.io/s/quiet-frost-wi7gk
Никаких лишних перерендеров, как при изменении родительского компонента, так и при изменениях в дочерних.

И вот голый React с его неадекватными рендерами всего и вся. codesandbox.io/s/romantic-hermann-ts6ny
Ну как бы вот тут MobX, банальный пример как именно он должен применяться. codesandbox.io/s/quiet-frost-wi7gk

Там вообще нет зависимости между родительским и дочерним компонентом. Еще бы оно не обновлялось — реакт всё же не настолько плохой, чтоб это перерисовывать.
А, ну да, пардон, там дело не в реакте, а в observer(). Там, собственно, такая же развязка сделана, как и в редаксовском connect().
Так что использовать, контексты или Redux?

Ни то, ни другое. Надо использовать (бесстыжая самореклама!) Statium: https://github.com/riptano/statium. RealWorld example: https://github.com/nohuhu/react-statium-realworld-example-app


State colocation из коробки, наглядно, и, что самое важное — с доступом к состоянию родительских компонентов, тоже из коробки. И многое другое.

Component state is contained in a ViewModel, which is a React Component implementing a hierarchically scoped key/value store with a few additional features.

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

Спасибо за критику, очень интересно. Можно по пунктам?


Да, давайте в компоненты еще и модель затащим.

А почему нет? Этот подход идеологически ничем не отличается от this.setState() в компонентах; практически разница только в том, что состояние делегировано в отдельный, предназначенный для этого компонент. Специализированность компонента позволяет решать проблемы доступа к данным без текущих абстракций, а то, что хранилище является компонентом, позволяет использовать все оптимизации, наработанные в React с его появления.


Внутри ViewModel используется именно setState, вполне ванильно.


Я, собственно, ничего особо против вашего проекта не имею, но вот загаживать DOM тем, чему в ней явно не место — это сильный моветон.

Подождите, какой DOM? ViewModel ничего не рендерит в DOM, это виртуальный компонент с хранилищем для данных. Или я как-то не так вас понял?

Подождите, какой DOM? ViewModel ничего не рендерит в DOM, это виртуальный компонент с хранилищем для данных. Или я как-то не так вас понял?

Да, пардон, я не посмотрел исходники до конца, и не увидел, что оно всё заканчивается контекстами.

А почему нет? Этот подход идеологически ничем не отличается от this.setState() в компонентах; практически разница только в том, что состояние делегировано в отдельный, предназначенный для этого компонент.

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

Фух, спасибо, отлегло. А то я уже в исходники полез, вдруг что-то где-то течёт… :)


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

Ага, вот тут уже понятнее. Если честно, я с вами вполне согласен — React это не самое лучшее решение, как идеологически, так и практически. Но по разным совершенно не техническим причинам, React есть реальность, данная нам в ощущениях. Можно это оспаривать сколько угодно, но если вы сравните количество вакансий на React со всем остальным, то...


В общем, в заголовке репозитория так и написано: Statium это прагматичный подход к управлению состоянием в React. :)


но «нет» в первую очередь потому, что состояние — это не компонент, и нет ни малейшей необходимости делать его таковым.

Состояние это не компонент, состояние это просто набор данных. Компонентом является хранилище состояния, фактически это просто API для доступа к объекту с данными.


Необходимо ли было делать это API в виде компонента? Нет конечно, есть и другие варианты; у этого же подхода есть как минимум одно большое преимущество: лёгкость использования в функциональных/презентационных компонентах. Гораздо проще и быстрее написать вот такой код:


const Component = () => (
    <ViewModel ...>
        ...
    </ViewModel>
);

… чем функцию, которая что-то с чем-то соединяет, или @observable магию, которая рассыпается без поддержки декораторов, etc.


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

Да, вы правы. В какой-то момент мне надоело бодаться с ветряными мельницами. If you cannot beat them, join them и всё такое.


Тем более, что если допустить, что с какой-то точки зрения React вполне себе работает, то может, и ладно? Работает себе, давайте используем существующие механизмы. В конце концов, зарплату платят за фичи. :)


Но выглядит это как нагромождение абстракций там, где вообще-то можно гораздо проще.

Вполне возможно, что оно так выглядит из-за плохого изложения материала. Над документацией мне ещё работать и работать, это факт. Потому что с моей точки зрения, абстракций в ViewModel как раз почти нет: это тупо тот же самый setState, только вынесенный в отдельный компонент.


И скорее всего это всё очень небесплатно для производительности.

Понятно, раздел с анализом производительности надо поднять наверх. :) Я его в конец README засунул, так не работает.


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


А если подумать, то чему там тормозить? Собственно данные ViewModel лежат в обычном объекте, который связан по прототипной цепочке с родительскими объектами. Чтение по ключу из объекта это как раз то, что JavaScript умеет делать очень быстро. :)


Обновление состояния тоже ненамного дороже родного для React this.setState(). А поскольку ключ обновляется в модели-владельце, то рендеринг отрабатывает только для этой модели и её детей. Ну т.е. в самом худшем случае, если у вас абсолютно все ключи в одной модели в корне приложения, получаете такой как бы аналог Redux. :)


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


А в Statium всё просто: каждая модель автоматически имеет доступ к родительским данным, и вы просто можете их использовать там, где нужно. И любимая проблема: упс, я задал ключ foo вот здесь, а он нужен ещё вон там в соседнем компоненте, решается банально переносом одного свойства в родительскую модель. И всё просто работает. :)

Вай какой ужас, я хочу это развидеть

Спасибо за отзыв. :) Можно поинтересоваться, что именно вызвало у вас такую реакцию? Концепция хранения данных в компоненте, иерархическая связь, кривизна API, или код примера? Или всё вместе? Ну правда, интересно же. :)

Все вместе, вы собрали самые плохие и громоздкие подходы) Ни кому не пожелаю такое увидеть на каком нибудь проекте и работать с этим)

Вот странно, какие всё же плохие и громоздкие подходы вы имеете в виду… На мой взгляд, Statium гораздо проще и понятнее того же Redux, да и MobX тоже.


А коллегам, между прочим, вполне понравилось. :)

Вот посмотрите как работает MobX, что тут может быть сложного? просто работа с классами и всё
codesandbox.io/s/goofy-hamilton-b9jxv

Спасибо за ссылку, я в MobX копал вполне глубоко. На мой взгляд, именно работа с классами является неестественным усложнением в большей части случаев.


Ну честно, вам когда форму с полями нужно сделать, зачем для этого нужно создавать класс, поля, методы для работы с ними, да ещё и обмазывать эти методы магией @observable? В Statium всё гораздо проще: вот модель (API), вот поля с данными (initialState), вот список, какие значения куда скормить (Bind). И всё.


А если в форме нужно показывать значение из вышестоящего состояния? Имя юзера, например, или поле из родительского компонента. В Statium банальнейше, в MobX больно (ручками).


Вы посмотрите пристальнее, вдруг понравится. От ненависти до любви один шаг. :)

Ну честно, вам когда форму с полями нужно сделать, зачем для этого нужно создавать класс, поля, методы для работы с ними, да ещё и обмазывать эти методы магией @observable?

И действительно, зачем? Что вам мешает сделать простой const model = observable({ бла-бла }) и залить туда всё нужное?

А куда вы этот код сделаете, и как будете вызывать, чтобы а) данные туда сложить, и б) данные оттуда вытащить? И когда?


Нет, мне правда интересно. Почему-то во всех примерах с MobX используются именно классы и тонна магии. Почему?

А куда вы этот код сделаете

Куда угодно.

а) данные туда сложить

Данные-то откуда? Из формы? Ну вот и складывайте где-то там, где у вас обработчики событий формы.

б) данные оттуда вытащить?

Эм. Взять этот обсервабл и прочитать, что в нём?

Нет, мне правда интересно. Почему-то во всех примерах с MobX используются именно классы и тонна магии. Почему?

Где же? Примеры из документации mobx очень часто написаны без декораторов.
Куда угодно.

Можно пример? Я, видимо, недостаточно глубоко копал MobX, мне не очевидно, как такой подход использовать.


Данные-то откуда? Из формы? Ну вот и складывайте где-то там, где у вас обработчики событий формы.

Из формы или ещё откуда-нибудь. В вашем примере мы присваиваем значение переменной. Эту переменную надо где-то хранить, чтобы потом использовать в форме. Надо создавать класс для компонента, чтобы хранить this.model, или функциональный компонент. Тоже ручная работа.


Но это всё ерунда, а вот как заставить компонент перерисовываться на изменениях модели? https://mobx.js.org/refguide/observable.html ни слова об этом не говорит, зато перечисляет аж 5 правил конвертации значений, которые нужно знать. И исключения для MobX версии 4, о которых тоже надо знать.


Зачем? Чтобы сделать форму с тремя полями?


Эм. Взять этот обсервабл и прочитать, что в нём?

Для этого надо сперва понять, что он изменился. См. предыдущий пункт.


Где же? Примеры из документации mobx очень часто написаны без декораторов.

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


Примеров с React без использования классовых компонентов я не нашёл. Плохо искал?

Можно пример? Я, видимо, недостаточно глубоко копал MobX, мне не очевидно, как такой подход использовать.

Ну come on, это просто переменная. Переменную можно объявить примерно где угодно, и передать примерно куда угодно.

Из формы или ещё откуда-нибудь. В вашем примере мы присваиваем значение переменной. Эту переменную надо где-то хранить, чтобы потом использовать в форме. Надо создавать класс для компонента, чтобы хранить this.model, или функциональный компонент. Тоже ручная работа.

Если у вас компонентный UI, то странно жаловаться, что надо, оказывается, эти самые компоненты делать, не?

Но это всё ерунда, а вот как заставить компонент перерисовываться на изменениях модели?

Очевидно же, что MobX этими вопросами не занимается — это просто реактивная библиотека. Как хотите, так и перерисовывайте — в общем случае это просто объявление reaction() или даже autorun() внутри компонента, который при изменении нужных свойств у обсервабла запустит перерисовку компонента. Это, собственно, именно то, что делает observer из mobx-react. Никакой магии тут нет.

Для этого надо сперва понять, что он изменился. См. предыдущий пункт.

Если вам нужно это понимать — для этого и существует все эти reaction(), autorun(), или даже просто observe(). Обычная подписочная модель.

Примеров с React без использования классовых компонентов я не нашёл. Плохо искал?

Я не знаю, как вы искали, потому что у меня поиск занял примерно 5 секунд — на то, чтоб набрать три слова и на то, чтоб гугл выдал результаты.
Ну come on, это просто переменная. Переменную можно объявить примерно где угодно, и передать примерно куда угодно.

Вы с React работаете? Я не просто так спрашиваю, мне интересен MobX в контексте React. Вне этого контекста он мне не интересен.


А в React просто переменная мало чем пригодится. Если вы её в модуле объявите, будет у вас один экземпляр observable, созданный в момент загрузки модуля. А компонент рендерится гораздо позже, что мне толку от такой переменной? А если мне надо два экземпляра такого компонента?


Если у вас компонентный UI, то странно жаловаться, что надо, оказывается, эти самые компоненты делать, не?

Я жаловался не на то, что надо компоненты делать. А на то, что в компонентах React надо будет вручную делать observable и обвязку к нему. Что, на мой взгляд, совершенно не упрощает код даже в сложных случаях.


Вы же сами выше по ветке утверждали, что всё элементарно просто:


И действительно, зачем? Что вам мешает сделать простой const model = observable({ бла-бла }) и залить туда всё нужное?

Вот я и прошу уточнить, как именно этот подход использовать в React + MobX, чтобы было просто, наглядно и очевидно. Я с вами не спорю, а пытаюсь учиться. Очевидно, что моё MobX-fu не так сильно, как я думал.


Очевидно же, что MobX этими вопросами не занимается — это просто реактивная библиотека. Как хотите, так и перерисовывайте — в общем случае это просто объявление reaction() или даже autorun() внутри компонента, который при изменении нужных свойств у обсервабла запустит перерисовку компонента. Это, собственно, именно то, что делает observer из mobx-react. Никакой магии тут нет.

Если вам нужно это понимать — для этого и существует все эти reaction(), autorun(), или даже просто observe(). Обычная подписочная модель.

Это, по-вашему, подпадает под определение "простой в использовании библиотект"?


Например, чем отличается reaction() от autorun()? Как определить, в каком случае использовать то, а в каком другое? А если окажется, что я ошибся и случай не тот, а другой? Как менять одно на другое? Как тестировать, что при замене моя функциональность не сломалась?


Зачем мне нужно знать, что MobX не занимается вопросами состояния компонентов в React, а для этого нужен react-mobx? Мне, блин, форму с тремя полями надо сделать.


Я не знаю, как вы искали, потому что у меня поиск занял примерно 5 секунд — на то, чтоб набрать три слова и на то, чтоб гугл выдал результаты.

Спасибо, я не догадался добавить hooks в поиск. Я искал "react mobx no class", и первый результат на StackOverflow указывает на то, что MobX хуки не поддерживает.


Из ваших результатов видно, что уже (вроде бы?) поддерживает, но не сам MobX, а сторонняя библиотека mobx-react-lite, у которой есть конфликты с существующим кодом. Поддержка хуков планируется в MobX 6, но в официальном репозитории MobX нет упоминания версии 6.


А если почитать чуть глубже, то начинаются ограничения: если хуки, то классовые компоненты нельзя. А если мне надо componentWillUnmount? Тогда старый MobX с классами, а он в хуки не умеет. Приехали обратно.


Вот честно, я уже потратил ещё один час на более глубокое ознакомление с MobX, и этот час совершенно не приблизил меня к пониманию, каким образом React + MobX может быть проще в использовании, чем React + Statium.


А форма с тремя полями? Засекайте время:


import ViewModel, { Bind } from 'statium';

const state = {
    username: '',
    email: '',
    password: '',
};

const FormWithThreeFields = () => (
    <ViewModel initialState={state}>
        <Bind props={[
                ['username', true], ['email', true], ['password', true]
            ]}>
            { values => (
                <form>
                    <input type="text" placeholder="User name"
                        value={values.username}
                        onChange={e => { values.setUsername(e.target.value); } />

                    <input type="email" placeholder="E-mail"
                        value={values.email}
                        onChange={e => { values.setEmail(e.target.value); }} />

                    <input type="password" placeholder="Password"
                        value={values.password}
                        onChange={e => { values.setPassword(e.target.value); }} />
                </form>
            )}
        </Bind>
    </ViewModel>
);

export default FormWithThreeFields;

… 3 минуты, это я опечатываюсь много.

А если мне надо два экземпляра такого компонента?

Значит, заводите переменную в компоненте.

А на то, что в компонентах React надо будет вручную делать observable и обвязку к нему.

В ваших примерах вам state = {} написать не лень, а вот observable() написать — будет лень, так что ли?

Это, по-вашему, подпадает под определение «простой в использовании библиотект»?

Конечно. Тут везде ровно одна механика — подписка на изменение, а как именно вы её выражаете — зависит от того, что вам нужно сделать.

Например, чем отличается reaction() от autorun()?

В документации написано. В одно короткое предложение написано, кстати.

Зачем мне нужно знать, что MobX не занимается вопросами состояния компонентов в React, а для этого нужен react-mobx? Мне, блин, форму с тремя полями надо сделать.

Задавая вопрос в таком контексте — вы неизбежно придете к тому, что для формы с тремя полями вам не нужна ваша библиотека, вам не нужен MobX, и уж тем более вам не нужен реакт. Стейт-менеджмент вообще не имеет не малейшего смысла (любой), если вам «просто нужна форма с тремя полями». Стейт-менеджмент становится крайне полезен, когда у вас зоопарк разных контролов со сложной логикой взаимодействия.

Из ваших результатов видно, что уже (вроде бы?) поддерживает, но не сам MobX, а сторонняя библиотека mobx-react-lite, у которой есть конфликты с существующим кодом.

Я не вполне понимаю ваш способ чтения, потому что в самом начале readme английским по белому явно написано, что mobx-react-lite может спокойно использоваться вместе с mobx-react.

А если почитать чуть глубже, то начинаются ограничения: если хуки, то классовые компоненты нельзя.

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

Отлично, мы завели переменную в компоненте. Что это даёт? Ничего, всё равно надо подписываться на обновления вручную. Вот как в примере markelov69 ниже: создаём observable, скармливаем его в хук useState, и… Понимаем, что observable вообще не нужен, потому что хук сам по себе и решает вопросы хранения состояния и подписки на изменения этого состояния.


Я что-то упустил?


В ваших примерах вам state = {} написать не лень, а вот observable() написать — будет лень, так что ли?

Дело не в лени, а в совершенно различной семантике. Объект, который вы передаёте в initialState, выполняет несколько функций:


а) Задаёт список ключей с (потенциально) их типами
б) Присваивает ключам значения по умолчанию, которые абсолютно необходимы для первого рендера (React синхронный, не забывайте!)
в) Устанавливает ограничения на дальнейшие обновления состояния. Если вы не задали ключ в initialState, то установить его позже не получится, полетит исключение.


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


Это очень сильно отличается от простого создания экземпляра observable в переменную, не находите?


Конечно. Тут везде ровно одна механика — подписка на изменение, а как именно вы её выражаете — зависит от того, что вам нужно сделать.

Так вот я эту механику никак не вижу в вашем примере с MobX — если, конечно, не делать её вручную. В чём и состоит суть моей аргументации: зачем вообще заниматься этим вручную? Зачем дублировать в каждом компоненте императивный код, когда можно вместо этого декларировать состояние и его отношение к вашим компонентам? И всё это делается очень легко, очень быстро и наглядно, без лишней писанины.


Я понимаю, что вы работаете с MobX, он вам нравится и кажется естественным. Мне же MobX видится очень неловким натягиванием совы на глобус, по крайней мере в приложении к React. Слишком много лишних телодвижений нужно совершить для решения очень простых задач, а со сложными я даже связываться не стал.


В документации написано. В одно короткое предложение написано, кстати.

Это тоже нужно знать, учитывать, и тратить на чтение документации время. И потом на эсперименты с выяснением, а что же на самом деле происходит в вашем контексте с вашим кодом.


Если каждый мелкий аспект использования инструмента даёт высокую когнитивную нагрузку, то вы вместо решения своих задач начинаете всё время тратить на изучение инструмента. Это неэффективно.


Задавая вопрос в таком контексте — вы неизбежно придете к тому, что для формы с тремя полями вам не нужна ваша библиотека, вам не нужен MobX, и уж тем более вам не нужен реакт. Стейт-менеджмент вообще не имеет не малейшего смысла (любой), если вам «просто нужна форма с тремя полями». Стейт-менеджмент становится крайне полезен, когда у вас зоопарк разных контролов со сложной логикой взаимодействия.

Вот тут, как мне кажется, мы стали нащупывать суть вопроса. Почему вам кажется, что для формы с тремя полями не нужно управление состоянием? Это не пассивно-агрессивная атака, мне в самом деле интересно, почему вы так думаете. Честно.


Не потому ли, что в популярных библиотеках (Redux, MobX) управление состоянием делается настолько сложно, что временные и когнитивные затраты несопоставимы с результатом? И для простейшей формы с тремя полями проще не связываться, а сделать руками?


Обратите внимание на популярность хуков. Почему сразу после их появления многие разработчики стали использовать их вместо Redux или MobX? На мой взгляд, именно по причине простоты использования: useState не требует никаких церемоний, создания констант-экшнов-редьюсеров-и-т.д., или классов с @observable, и т.п.


К сожалению, у хуков очень много недостатков. Пресловутые Rules of Hooks, трудности с тестированием, трудности с доступом к состоянию из дочерних компонентов, потеря декларативности презентационных компонентов, масса ручной работы, потеря функциональной чистоты компонентов, случайные баги… Хуки useState и useReducer это не самый плохой компромисс, но всё же плохой.


А у Statium ни одного из этих недостатков нет, и использовать ViewModel очень просто. Настолько просто и очевидно, что вопрос стоит уже по-другому: а какова может быть причина не использовать state management?


Тем более такой, который позволяет совершенно безболезненно и за 10 минут масштабироваться от формы с тремя полями до таблицы с фильтрацией записей в реальном времени, например: https://gist.github.com/nohuhu/01a7bb42b1c1e1378ea0df6829799055


Я не вполне понимаю ваш способ чтения, потому что в самом начале readme английским по белому явно написано, что mobx-react-lite может спокойно использоваться вместе с mobx-react.

Первый абзац из документации mobx-react-lite гласит, что:


Class based components are not supported except using <Observer> directly in class render method. If you want to transition existing projects from classes to hooks (as most of us do), you can use this package alongside the mobx-react just fine. The only conflict point is about the observer HOC. Subscribe to this issue for a proper migration guide.

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


К тому же, если речь идёт о миграции с классов на хуки, то зачем вообще MobX? Я так до сих пор и не понимаю.


А если почитать еще чуть глубже нормальным способом чтения, то там опять же явно написано, что mobx-react-lite вполне применим в классовых компонентах.

Ещё раз, зачем мне читать глубже? Это вы предлагаете мне использовать MobX, потому что он гораздо лучше и проще моего решения. Я просто не понимаю, каким образом библиотека, которая фокусируется на других вещах (первый абзац из документации):


"MobX in React brings easy reactivity to your components. It can handle whole application state as well."

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


Ещё раз: я понимаю, что лично вам MobX нравится и кажется лучшим решением. А лично мне он не кажется даже просто хорошим решением. Слишком много усложнений для чего? Easy reactivity? Так мне оно не нужно (что бы это ни было), мне нужно форму с тремя полями. Или таблицу с фильтрацией.

Мне кажется или вы так и посмотрели на код? codesandbox.io/s/admiring-ishizaka-ncsfk

Просто посмотрите на код и как это работает и всё что вы написали должно отпасть сразу
Раз вы такое пишете, значит вы вообще не копали MobX и даже не пробовали его в деле
Раз вы такое пишете, значит вы вообще не копали MobX и даже не пробовали его в деле

Нет, в больших приложениях, построенных исключительно на MobX, не пробовал. Пробовал а) построить на нём мелкий проект, б) внедрить в большом проекте, где state management делался вручную на классовых компонентах и setState. В процессе копал достаточно (как мне показалось) глубоко, но скорее всего в какую-то другую сторону.


Вот, прям сходу на коленке, codesandbox.io/s/admiring-ishizaka-ncsfk

Брр, вы меня теперь уже совсем запутали. Если вы используете хук useState, то зачем вообще нужен MobX?

Брр, вы меня теперь уже совсем запутали. Если вы используете хук useState, то зачем вообще нужен MobX?

useState тут используется просто как конструктор, который возвращает то, что возвращает функция которая в него прокинута, он никакой другой роли не играет и благодаря нему ничего не обновляется, он чисто для того, чтобы переменная state всегда ссылалась на один и тот же экземпляр созданного класса.
class App extends React.Component {
    constuctor(){
        this.state = new State();
    }
}

Это по сути тоже самое, только для функционального компонента.

Так вы сам код посмотрели? Насколько все красиво, и элементарно на самом деле то? Не то, что ваши адские нагромождения и лапшекод. React кайфовый только лишь в связке с MobX, все остальное это дно полнейшее. Если не использовать React+MobX, то надо брать Vue или Svelte.
Так вы сам код посмотрели?

Да я-то ваш код смотрел. Это вам я никак не могу донести свою мысль, видимо плохо объясняю.


Давайте попробуем вот так: https://codesandbox.io/s/xenodochial-snyder-hngmw


Так зачем MobX-то? :)

В кратце так, mobx мутабельный и рекативный, вот почему) А React сам по себе имутабельный и не реактивный.
Лишние рендеры голый реакт против mobx:

codesandbox.io/s/quiet-frost-wi7gk — react+mobx

codesandbox.io/s/romantic-hermann-ts6ny — react
В кратце так, mobx мутабельный и рекативный, вот почему) А React сам по себе имутабельный и не реактивный.

Т.е. теперь вы продаёте мне MobX под волшебным соусом якобы более высокой производительности? Ну нет, спасибо, такой перевод темы мне не интересен. Если вы уже забыли, дискуссия началась с того, что управление состоянием в приложении должно делаться просто и легко, потому что нет никаких весомых причин к обратному. Исследовать альтернативную вселенную псевдонаучного еслибдакабизма на тему потенциального выигрыша производительности мне недосуг.


Просто потому, что я в эту тему копал очень глубоко (нет, не в MobX, в прозводительность), и самый главный урок оказался простым: premature optimization is the root of all evil. ©

Вот смотрите, таблица с асинхронной загрузкой записей, фильтрацией и страницами: https://codesandbox.io/s/elated-buck-29fgg


За 10 минут, да. Где адские нагромождения-то? :)

Оба ваших примера, и форма, и таблица — перерисовывают всё на том уровне, на котором у вас самая нижняя ViewModel. То есть форма перерисовывается вся при изменении любого одного контрола, потроха таблицы после Bind — тоже все разом. Чтоб перерисовывать только то, что меняется (как это и происходит в вариантах с MobX) — вам нужно тащить ViewModel вниз.

ЗЫ: Вы, конечно, можете сказать, что перерисовывать 4 инпута вместо одного — это фигня и нестрашно, но то же самое справедливо и для MobX — эту же форму можно нарисовать обычным реактом, не трогать (на этом уровне) MobX и не написать ни одного лишнего слова в коде.
То есть форма перерисовывается вся при изменении любого одного контрола, потроха таблицы после Bind — тоже все разом.

Так они и обязаны перерисовываться, потому что зависят от состояния ключей filter, page и limit. Напрямую зависят. Я не вижу, в чём здесь проблема.


Чтоб перерисовывать только то, что меняется (как это и происходит в вариантах с MobX) — вам нужно тащить ViewModel вниз.

Можно и утащить, если есть необходимость. Можно добавить кучу оптимизаций, чтобы не пересчитывать values каждый раз, мемоизировать формулу, например. Вопрос в другом: зачем? Если написанное в лоб, за 10 минут, решение работает с адекватной скоростью, зачем мне тратить ценное время на предварительные оптимизации? Будет проблема, будем решать.


ЗЫ: Вы, конечно, можете сказать, что перерисовывать 4 инпута вместо одного — это фигня и нестрашно, но то же самое справедливо и для MobX — эту же форму можно нарисовать обычным реактом, не трогать (на этом уровне) MobX и не написать ни одного лишнего слова в коде.

Во-первых, именно так и скажу: если перерисовка 1000 компонентов <tr>...</tr> в таблице отрабатывает за ~18 мс, то перерисовка 4 полей тоже проблем не вызовет. Да хоть 20.


Во-вторых, конечно эту форму можно нарисовать на ванильном React, вообще без MobX. Но а) не за 10 минут, б) не декларативно, в) доступ к данным будет ручками. А ручками как раз очень не хочется, потому что больно, чревато багами и тестировать трудно.


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


А вот как замена ванильному useState и хукам Statium даёт очень заметные преимущества. Это примерно как если пересесть с телеги на болид Формулы-1. Вы попробуйте как-нибудь, просто для расширения горизонтов. Вдруг понравится. :)


Для меня, кстати сказать, опыт с RealWorld example был очень поучительным. Некоторые вещи оказались не такими удобными, как казалось при разработке библиотеки, но многое другое оказалось даже лучше, чем я предполагал. Надо ещё что-нибудь подобное замутить, dogfooding это всегда полезно...

image
Вот этот блок уже сам по себе уродство и адское нагромождение. И это при условии что табличка то не из реальной жизни, у вас всего 3 колонки, в реальности их обычно 10 и больше.
Вы объединили тут 3 убогих подхода:
1) Render props
2) Wrapped hell
3) Props hell
Вот этот блок уже сам по себе уродство и адское нагромождение.

Я смотрю, наша дискуссия уже окончательно пала до вкусовщины? Можем не продолжать в таком случае, всё равно бессмысленно.


И это при условии что табличка то не из реальной жизни, у вас всего 3 колонки, в реальности их обычно 10 и больше.

Да сколько угодно колонок может быть. Какая разница, сколько их? Я ведь говорил, что это пример, написанный за 10 минут. В реальном коде никто не мешает декомпозировать, использовать HOC или хуки. Три варианта, на выбор.


Вы объединили тут 3 убогих подхода:

Тут даже не знаю, что ответить. Вы в курсе, что Render props являются вполне официально рекомендованным подходом? Про остальные пункты я даже не слышал, вы их только что придумали наверное.


Извините, но если вы в таком пренебрежительном тоне отметаете рекомендации разработчиков React, то либо потрудитесь предоставить веское обоснование, либо давайте закончим эту беседу.

Да мне не нужно вам ничего доказывать :D Пишите как угодно, лишь бы это не трогало других людей) Я просто сочувствую тем, кому потом достанется ваше чудо) Но обычно просто такие проекты переписываются с нуля, т.к. bus factor

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


  1. Стоит убрать FaaC (function as a child) в пользу хуков. Сильно сократит и упростит код. В разы.


  2. Показал бы в примерах как это тестировать.


  3. Как вызвать метод контроллера компонента сверху? (Окей, диспатч или более странный способ через $set, допустим. К слову, в редаксе нельзя напрямую писать в store, никогда не задумывались почему?)


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


  5. У меня есть список пользователей. Каждый раз я открываю пользователя — я должен его догрузить. Но у меня еще есть N методов для каждого пользователя. Как вы это организуете в своем фреймворке? Каждый пользователь — отдельный ViewModel? А как тогда будете кешировать пользователей?


  6. Продолжение 5-го пункта. У меня в реальном проекте более 50 доменных объектов (это мы еще плохо нормализовали данные). И они, к сожалению, достаточно сильно связаны между собой. У вас вырадится в редакс? Или будет иерархия из 50-ти вложенных контекстов?


    Как "прервать" / отбросить / отменить асинхронный метод? Что если я уже успел покинуть viewModel, которое еще не успело что-то загрузить, при этом оно перепишет какое-то значение родителя?



    К слову, как в методе контроллера получить значение чужой viewModel?



    Самое главное: зачем это все, если есть useReducer и useContext, которые позволяют получить все тоже самое? Ну вместо $set будет у вас dispatch({type: 'set'}), обертку можно легко сделать.
    P.S. странно, у последних абзацев должны быть цифры 7, 8 и 9, не знаю что поломалось.



Спасибо за критику! Нет, правда, мне очень интересны критические/негативные отзывы. Это возможность узнать о проблемах и быстрее их решить. :) Текущая версия библиотеки всего 0.2.3, ещё многое нужно решить! И API тоже можно менять, в разумных пределах. :)


Далее по пунктам (без номеров, markdown странно работает в цитатах):


Абстрагируясь от идеи вписывания бизнес-логики во вью

Мне кажется, тут скорее проблема с подачей материала (с моей стороны, буду править). Бизнес-логика вписывается не в презентационный слой, а в ViewController. Это сама по себе отдельная сущность, которая занимается только асинхронной логикой. <ViewModel controller={...} /> это просто сокращение, чтобы отдельно ViewController не описывать (и сэкономить на отступах! :), а вообще ничто не мешает описывать логику вообще отдельно от всего, например так:


import { ViewController } from 'statium';

const MyController = ({ children }) => (
    <ViewController initialize={...} handlers={...} >
        { children }
    </ViewController>
);

export default MyController;

… и использовать потом как изолированный компонент.


Стоит убрать FaaC (function as a child) в пользу хуков. Сильно сократит и упростит код. В разы.

Вы имеете в виду FaaC в компоненте Bind? Можно вместо него использовать хук useBindings:


import { useBindings } from 'statium';

const MyComponent = () => {
    const [foo, bar] = useBindings('foo', 'bar');

    ...
};

Что интересно, я тоже думал, что хуки сократят и упростят код компонентов. На практике оказалось, что мне самому существенно быстрее и проще использовать <Bind> и withBindings, хотя хук тоже иногда пригождается.


Показал бы в примерах как это тестировать.

Обязательно! Тестирование — это одна из сильных фишек Statium. К сожалению, я не могу показать код, который мы для тестирования боевых приложений используем, но обязательно добавлю примеры тестирования в RealWorld приложение. Просто не успел ещё, я его в прошлые выходные запилил по-быстрому. :)


Как вызвать метод контроллера компонента сверху? Окей, диспатч или более странный способ через $set, допустим. К слову, в редаксе нельзя напрямую писать в store, никогда не задумывались почему?)

а) Вызвать метод родительского контроллера: $dispatch.


б) $set устанавливает значение в модели-владельце. Это не обязательно родительская ViewModel. $set карабкается по дереву, находит ближайшего владельца и устанавливает значение. Если какая-то из дочерних моделей переназначает ключ, то обновляться будет она (из низлежащих). Надо будет добавить инструментарий для отслеживания таких ситуаций.


в) В Statium тоже нельзя напрямую писать в данные ViewModel, только через функцию-setter. При этом можно либо валидировать значения перед записью (applyState), либо полностью предотвращать запись и делать что-то ручное (protectedKeys + ViewController event handler). По умолчанию модель просто обновляет значение, потому что это самый частый случай.


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

Объединить оба компонента под родительской ViewModel и вынести связанные ключи в неё. Это безболезненно и очень быстро. :) И в смысле разработки, и в смысле производительности.


Это одна из самых важных фич ViewModel: обновления состояния всегда ограничены минимально возможным регионом. Автоматически.


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


У меня есть список пользователей. Каждый раз я открываю пользователя — я должен его догрузить. Но у меня еще есть N методов для каждого пользователя. Как вы это организуете в своем фреймворке? Каждый пользователь — отдельный ViewModel? А как тогда будете кешировать пользователей?

Самый очевидный вариант — да, рендерить ViewModel на каждого пользователя. Я в примере так и сделал со списком статей, проблем с производительностью не заметил при N = 100. N = 500 ломает Chrome, но это почему-то происходит и без ViewModel на странице, я копать не стал (времени не было). Т.е. при рендеринге 100 ViewModel при профилировке этих вызовов вообще не заметно в Call Trace.


Про кеширование не совсем понял, уточните?


Продолжение 5-го пункта. У меня в реальном проекте более 50 доменных объектов (это мы еще плохо нормализовали данные). И они, к сожалению, достаточно сильно связаны между собой. У вас вырадится в редакс? Или будет иерархия из 50-ти вложенных контекстов?

Тоже не совсем понял. 50 вложенных объектов, или просто связанных? В ViewModel можно хранить объекты, и вообще любые структуры данных. Селекторами можно лезть глубоко в структуру, как для чтения, так и для записи:


import ViewModel, { useBindings } from 'statium';

const initialState = {
    foo: {
        bar: {
            baz: 42,
        },
    },
};

const Component = () => (
    <ViewModel initialState={initialState}>
        <CounterButton />
    </ViewModel>
);

const CounterButton = () => {
    const [[baz, setBaz]] = useBindings([['foo.bar.baz', true]]);

    return (
        <button onClick={() => { setBaz(baz + 1); }}>
            Click me!
        </button>
    );
};

Никакой магии тут нету, кстати сказать, банальные lodash.get/set. Ну, и чуточку обвязки.


Как "прервать" / отбросить / отменить асинхронный метод? Что если я уже успел покинуть viewModel, которое еще не успело что-то загрузить, при этом оно перепишет какое-то значение родителя?

Вы мысли мои читаете? :)) Только час назад открыл этот тикет: https://github.com/riptano/statium/issues/13.


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


С архитектурной точки зрения мне кажется, что таких штук допускать не надо. Данные надо держать близко к коду. На практике, конечно, нужно с этим что-то делать. Идеи приветствуются. :)


Да, а вот вариант с перезаписыванием значения в родительской ViewModel мне в голову пока не приходил, спасибо. Т.е. в случае:


const Parent = () => (
    <ViewModel initialState={{ foo: 'bar' }}>
        <Child />
    </ViewModel>
);

const Child = () => (
    <ViewModel initialState={{ bar: 'qux' }}
        controller={{
            handlers: {
                event,
            },
        }}>
        ...
    </ViewModel>
);

const event = async({ $set }) => {
    await new Promise(resolve => { setTimeout(resolve, 10000); });
    $set('foo', 'baz');
};

… если event уже выстрелил, а потом Child демонтируется, то… хм… Надо тест написать, я не уверен, что произойдёт в текущем коде. Скорее всего значение foo изменится на новое, без ошибок.


К слову, как в методе контроллера получить значение чужой viewModel?

Если это родительская модель, то просто $get('foo') — все ключи вышестоящих моделей доступны детям (но не наоборот). Если это не родственная модель, то никак. Это фича. Выносите ключи в общего родителя.


Самое главное: зачем это все, если есть useReducer и useContext, которые позволяют получить все тоже самое? Ну вместо $set будет у вас dispatch({type: 'set'}), обертку можно легко сделать.

Так Statium и есть эта самая обёртка, чтобы вручную в каждом компоненте не писать. :) Точнее, первоначальная версия ViewModel была такой обёрткой, 100 строк кода с useContext и useReducer. А зачем? Чтобы управление состоянием вынести за скобки компонента и перестать им заниматься. И думать о нём. Вообще.


Кроме этого: иерархический доступ к моделям. Этого вручную на хуках по-быстрому не сделать.


Или вот для синхронизации состояния с URL, легко и непринуждённо: https://github.com/riptano/urlito.


А ещё для метапрограммирования. Чтобы иметь компонент для динамического определения формата состояния компонентов, и обработки этого состояния. Одно из боевых применений в наших приложениях — компонент Report, который принимает объект с определением отчёта, полей, связей между ними, состояния таблиц, и т.д.; создаёт ViewModel и рендерит все дочерние компоненты. Очень удобно, когда встроенных отчётов 50+ в разных форматах, и нужно периодически добавлять новые.


ViewController рождён в битве с back end и выкован в схватках с Apollo GraphQL. В какой-то момент back end сказало: ква, быстрее не можем. Грузите данные в tree grid порциями: сперва верхний ряд строк, потом догружайте по необходимости. Попытки выкрутить хуки GraphQL и сделать такую ступенчатую загрузку с промежуточными состояниями и маскировкой таблицы в синхронном коде довели меня до белого каления, я плюнул и добавил к ViewModel ещё и ViewController. А потом добился разрешения всё это выложить в open source. :)


Это RealWorld приложение я слабал на коленке в прошлые выходные. Сам код Statium в тестировании уже более полугода, полёт номинальный. Скоро релиз. :)

Объединить оба компонента под родительской ViewModel и вынести связанные ключи в неё. Это безболезненно и очень быстро. :)

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


Я плохо представляю себе, насколько объемной должна быть модель, чтобы выборка данных стала бутылочным горлышком.

Да запросто. У вас есть 50 записей чего-то + фильтр (ну или сортировка). И черт с самим фильтром, быстро работает, а вот сбрасывание shallow compare (ну или shouldComponentUpdate) — большая проблема. Нужны уже реселекторы всякие там, чтобы computed всегда возвращал одно и то же значение.


Ах да, прошлый раз забыл: а что там по рефакторингу? Если я захотел переименовать user в profile — мне идти по всему коду и менять надпись? Селекторы как абстракция придуманы не просто так (в mobx это проще, потому что всегда прямая ссылка на поле, ts справится с переименованием).


Про кеширование не совсем понял, уточните?

Если я открыл страницу с ресурсом id = 1, затем id = 2, затем вернулся на 1 — зачем мне его перезагружать?


Тоже не совсем понял. 50 вложенных объектов, или просто связанных?

Нормализованных. Пример: у меня есть список всех лекций, а на панели слева список текущих лекций. Я переименовал одну лекцию — как панель слева узнает о том, что я это сделал? Логично использовать нормализованную структуру:


{
    byId: { 1: { title: 'Math' }, 2: { title: '' }},

    onGoing: [{id: 1, teacher: 2}, {id: 2, teacher: 5}],
}

Изменения в одном месте отражаются сразу во всем приложении. Только у меня таки "таблиц" в терминах БД 50+. И, как я сказал, если их хранить все в рутовом ViewModel, тогда он вырождается в редакс (только без тайм тревела, логирования, мидлвейров, саги). Если их хранить в виде иерархии ViewModel — как они будут сигнализировать об обновлении?


Никакой магии тут нету, кстати сказать, банальные lodash.get/set.

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


С архитектурной точки зрения мне кажется, что таких штук допускать не надо.

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


Если это не родственная модель, то никак. Это фича. Выносите ключи в общего родителя.

Это приведет, со временем, к миграции всего в рутовый ViewModel. В лучшем случае, в рутовый для модуля, а не всего приложения.


Простой пример: у вас есть список предметов ученика с оценками по ним. Идем по пути "Самый очевидный вариант — да, рендерить ViewModel на каждого пользователя.". Скажем, по каждому предмету еще тыкать можно, оценки там менять, заметки добавлять, преподавателя смотреть. Появляется задача — показать средний балл. Что ж, поехали все наши ViewModels для каждого предмета вверх по стейту и теперь есть общий ViewModel, который хранит список предметов. А теперь вам нужно эту оценку для каждого ученика в левой панели показать — опять все всплывает вверх. А потом среднюю по всем ученикам… И динамику, заодно.

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

Навскидку, странный общий ключ у общего родителя и в Statium вполне сработает.


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


В принципе горизонтальная сигнализация вполне возможна, но API для неё в рамках React придумать будет нетривиально. Я подумаю, задача интересная. :)


Да запросто. У вас есть 50 записей чего-то + фильтр (ну или сортировка). И черт с самим фильтром, быстро работает, а вот сбрасывание shallow compare (ну или shouldComponentUpdate) — большая проблема. Нужны уже реселекторы всякие там, чтобы computed всегда возвращал одно и то же значение.

Не уверен, что до конца понял постановку проблемы, но что-то подобное должно работать вполне быстро: https://gist.github.com/nohuhu/01a7bb42b1c1e1378ea0df6829799055


Набросал за 10 минут, попробовал — при отображении 1000 записей фильтрация отрабатывает с едва заметной задержкой. В профилировщике самая медленная функция вне React — это FaaC в теле компонента Table, отрабатывает за 1.8 мс собственного времени. Это подход влоб, без оптимизаций.


У меня, конечно, мощный лаптоп и условия не приближенные к боевым, но я как-то плохо представляю себе отображение 1000 записей на экране даже телефона.


Или я всё же как-то неправильно понял задачу?


Ах да, прошлый раз забыл: а что там по рефакторингу? Если я захотел переименовать user в profile — мне идти по всему коду и менять надпись? Селекторы как абстракция придуманы не просто так (в mobx это проще, потому что всегда прямая ссылка на поле, ts справится с переименованием).

Не совсем понятно, что вы имеете в виду под селекторами. Если меняется имя ключа в модели, то конечно же нужно менять привязки. Как вариант, можно использовать HOC:


import { withBindings } from 'statium';

const withFoo = Component => withBindings('foo')(Component);

В таком случае имя ключа надо будет менять только в одном месте.


На практике, так ли часто это нужно? В наших проектах TypeScript не используется, и вроде проблем особых нет. Не то, чтобы я был так уж активно против TypeScript, просто стараюсь исходить из прагматических соображений: есть проблема — решаю. Проблем с рефакторингом пока не было, тесты решают всё. :)


Нормализованных. Пример: у меня есть список всех лекций, а на панели слева список текущих лекций. Я переименовал одну лекцию — как панель слева узнает о том, что я это сделал?

Если у вас данные в нормальном виде, то самый простой подход — использовать вычисленные значения для презентации, с помощью формул: https://github.com/riptano/statium#computed-values. Я как раз такой подход использовал в примере выше. При этом ничто не мешает хранить нормальные данные в родительской модели, а формулы декларировать в дочерних, по необходимости.


Изменения в одном месте отражаются сразу во всем приложении. Только у меня таки "таблиц" в терминах БД 50+. И, как я сказал, если их хранить все в рутовом ViewModel, тогда он вырождается в редакс (только без тайм тревела, логирования, мидлвейров, саги).

Ну, не совсем вырождается в Redux, хотя такой вариант возможен. У вас ведь приложение состоит не только из списка лекций? Есть какие-то другие страницы: календарь, пользовательский профиль, профиль студента, наверняка куча всего. Если вы абсолютно всё состояние приложения храните в front end, то может быть и имеет смысл хранить все данные в глобальной модели. А может и нет; в Redux у вас особо выбора нет, а в Statium есть.


Что касается недостатков, то я не спорю, по фичам Statium очень далеко до Redux. Но и time travel, и logging вполне возможны, я планирую их добавить в какой-то момент. Просто инструментарий пока не в приоритете (дед лайн близко).


А Middleware делается через applyState и контроллеры.


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

Если для вас это недостаток, то можно не использовать. :) Глубокая выборка из структуры опциональна и сделана больше для удобства (пример). А так никто не мешает привязываться по ключу к структуре данных и ходить по ней явным образом.


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

Вот я как раз вчера думал на эту тему. И придумал вот что: можно отменять запросы через какое-нибудь новое API, но это чревато побочными эффектами. А что, если просто бросать исключение? Если родительская модель (или контроллер) отмонтирована, то попытка вызвать $get/$set/$dispatch бросит что-нибудь типа ComponentUnmountedError. А пользовательский код может это проверить и отреагировать, как надо — включая отмену сетевых запросов и т.п.


Как думаете? Исключения в обработчиках всё равно надо ловить, так что дополнительная когнитивная нагрузка минимальна.


Это приведет, со временем, к миграции всего в рутовый ViewModel. В лучшем случае, в рутовый для модуля, а не всего приложения.

Может привести, а может и нет. Всё зависит от того, какое у вас приложение, с чем работает, и как. Вы же исходите из своей специфики, а я, например, из своей. Для того типа приложений, с которыми я работаю, хранение глобального состояния в клиенте нетипично; чаще нужно хранить локальную копию состояния в клиенте, и обновлять её с сервера/в сервер при навигации и прочих действиях пользователя. Зачем тут глобальная модель, в которой всё-всё-всё лежит?


Вообще надо сказать, что с "толстыми" приложениями в web я отлично знаком — раньше работал над фреймворком Ext JS, который как раз для таких приложений больше использовался. И специфику хранения больших наборов нормализованных данных с моделированием отношений между ними я тоже знаю не понаслышке. :)


… опять все всплывает вверх.

Если всё всплывает вверх, то значит, это так нужно и логично. Не вижу противоречий. Просто исходя из моей практики, далеко не всё и не всегда всплывает вверх. Если локализовывать состояние в ViewModel, даже большое количество этого состояния, то получается такой мини-Redux для каждого модуля (страницы?). Ничего странного в этом нет.


А вот когда она нужна, локализация состояния делается в Statium очень легко и просто. А в Redux не очень, поэтому обычно все и говорят, что она не нужна. :)

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

Это, кстати, тоже преимущество Statium: не нужно никаких церемоний, изменения архитектурных решений и т.п. Можно просто взять и попробовать его использовать для одной маленькой формы. А потом для двух. А потом для всего модуля. Или приложения. Или не использовать, а продолжать с Redux.


И взаимодействие что с Redux, что с MobX сделать очень просто. По-моему, это большой плюс. Не находите?

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

Давно интересуюсь, а какие недостатки держать все контексты на самом верху, что плохого? Память? Производительность?
Типа вот так:
import React from 'react';

import Provider1 from 'contexts/context1.js';
import Provider2 from 'contexts/context2.js';
import Provider3 from 'contexts/context3.js';
import Provider4 from 'contexts/context4.js';

import App from './App.js';

const Root = () =>
  <Provider1>
  <Provider2>
  <Provider3>
  <Provider4>
    <App />
  </Provider4>
  </Provider3>
  </Provider2>
  </Provider1>

export default Root;


Вроде ж удобнее, не размазано по всему дереву.
Ререндер только того компоннета где дёргается useContext.
Смотря что вы имеете в виду под удобством. Давайте вообще забудем про производительность, оптимизацию и тд, и посмотрим на это исключительно с точки зрения удобства разработки. Есть у нас глобальный стейт который нужен везде, ну там, имя юзера, например — имя юзера отображается во всем приложении. Он глобален и его место, следовательно, в глобальном стейте. Мы просто не можем положить его куда-то ниже.

А есть какая-нибудь форма, значения которой вообще больше нигде не нужны. Зачем делать ее глобальной? Почему бы не держать ее стейты там же где находиться эта форма? Это не «размазывание» стейтов, мы просто держим стейты там где они и нужны. Стейты это же не какая-то абуза, это часть приложения, часть наших компонентов.

Это очень да же в логике самого по себе компонентного подхода, когда мы все держим «ближе к телу». Лично я предпочитаю всегда держать все что относиться к определенному компоненту как можно ближе к нему — стили, тесты, что угодно. Таким образом и работать с этим компонентом проще, ведь все что имеет отношение к этому компоненту не “размазано” по приложению.
мы все держим «ближе к телу»

Понравилась фраза)
Полностью с вами согласен.
Просто, в моём понимании, то что «ближе к телу» — это и есть обычный стейт, не контекст. Там где проп-дриллинг ещё терпимый.
Другое дело, если есть предпосылки/чуйка что данные стейта в будущем могут понадобиться чёрти где по всему приложению, то делаю контекст; и все их держу в одном месте на самом верху. Имя контекста соответствует предметной области за которую он отвечает. Вроде удобнее.
Интересовало именно, влиеяет ли это реально как-то на перформанс. Кажется нет)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории