Как сделать React приложение быстрее при помощи совместного размещения состояний +5


AliExpress RU&CIS

В статье речь идет о state colocation, то есть о совместном размещении состояний, этот термин можно было бы еще перевести как стейт колокейшн или стейт колокация.


Одной из основных причин замедления работы React приложения является его глобальное состояние (global state). Я покажу это на примере очень простого приложения, после чего приведу более близкий к реальной жизни пример.


Вот простенькое приложение в котором можно ввести имя для собаки (если окно не работает, вот ссылка):



Если вы поиграете с этим приложением то вскоре обнаружите что оно очень медленно работает. При взаимодействии с любым полем ввода возникают заметные проблемы с производительностью. В такой ситуации можно было бы использовать спасательный круг в виде React.memo и обернуть им все компоненты с медленным рендером. Но давайте попробуем решить эту проблему по другому.


Вот код этого приложения:


function sleep(time) {
  const done = Date.now() + time
  while (done > Date.now()) {
    // спим...
  }
}

// представим что этот компонент медленный
// из-за того что он рендерит кучу информации
function SlowComponent({time, onChange}) {
  sleep(time)
  return (
    <div>
      Wow, that was{' '}
      <input
        value={time}
        type="number"
        onChange={e => onChange(Number(e.target.value))}
      />
      ms slow
    </div>
  )
}

function DogName({time, dog, onChange}) {
  return (
    <div>
      <label htmlFor="dog">Dog Name</label>
      <br />
      <input id="dog" value={dog} onChange={e => onChange(e.target.value)} />
      <p>
        {dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}
      </p>
    </div>
  )
}

function App() {
  // это наше "глобальное состояние" (global state)
  const [dog, setDog] = React.useState('')
  const [time, setTime] = React.useState(200)
  return (
    <div>
      <DogName time={time} dog={dog} onChange={setDog} />
      <SlowComponent time={time} onChange={setTime} />
    </div>
  )
}

Если вы еще не читали статью про Colocation (совместное размещение), предлагаю вам ее прочитать. Зная что совместное размещение может упростить работу с нашим приложением, давайте используем этот принцип при работе с состояниями.


Обратите внимание на код нашего приложения, а именно на состояние time — оно используется каждым компонентом нашего приложения, поэтому оно и было поднято (lifting — поднятие состояния) в компонент App (компонент который оборачивает все наше приложение). Однако "собачье" состояние (dog и setDog) используется только одним компонентом, оно не нужно в компоненте App, так что давайте переместим его в компонент DogName:


function DogName({time}) { // <- убрали передачу состояний
  const [dog, setDog] = React.useState('') // <- добавили хук
  return (
    <div>
      <label htmlFor="dog">Dog Name</label>
      <br />
      <input id="dog" value={dog} onChange={e => setDog(e.target.value)} />
      <p>
        {dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}
      </p>
    </div>
  )
}

function App() {
  // это наше "глобальное состояние" (global state)
  const [time, setTime] = React.useState(200)
  return (
    <div>
      <DogName time={time} /> // <- убрали передачу состояний
      <SlowComponent time={time} onChange={setTime} />
    </div>
  )
}

И вот наш результат (если окно не работает, ссылка):



Вот это да! Ввод имени теперь работает ЗАМЕТНО быстрее. Более того, компонент стало легче поддерживать благодаря совместному размещению. Но почему оно стало работать быстрее?


Говорят что лучший способ сделать что-то быстро — это делать как можно меньше вещей. Это именно то, что здесь происходит. Когда мы управляем состоянием расположенным в самом верху дерева компонентов, каждое обновление этого состояния приводит к аннулированию всего дерева. Реакт не знает что изменилось, из-за этого ему приходиться проверять все компоненты чтобы понять нужны ли им обновления DOM. Этот процесс не бесплатен и потребляет ресурсы (особенно если у вас есть преднамеренно медленные компоненты). Но если вы переместите состояние как можно ниже в дереве компонентов, как мы это сделали с состоянием dog и компонентом DogName, то Реакт будет делать меньше проверок. Реакт не будет проверять компонент SlowComponent (который мы сделали преднамеренно медленным), так как Реакт знает что этот компонент все равно не может повлиять на вывод.


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


В реальной жизни


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


Одним из решений этой проблемы является "отмена" взаимодействия с пользователем (то есть, мы дожидаемся пока пользователь прекратит печатать, и тогда применяем обновление состояния). Иногда, это лучшее что мы можем сделать, но это может привести к плохому пользовательскому опыту (будущий concurrent mode должен свести необходимость так делать к минимуму. Посмотрите это демо от Дэна Абрамова).


Еще одним решением, которое часто применяют разработчики — это применение одного из спасательных рендеров Реакта, например React.memo. Это будет очень хорошо работать в нашем надуманном примере, так как позволяет Реакту пропустить повторную визуализацию SlowComponent, но на практике приложение может пострадать из-за «смерти от тысячи порезов», так как в реальном приложении замедление работы обычно происходит не из-за одного медленного компонента, а из-за недостаточно быстрой работы множества компонентов, так что вам придется применять React.memo повсюду. Сделав это, вы будете должны начать использовать useMemo и useCallback, иначе вся работа, которую вы вложили в React.memo, окажется напрасной. Эти действия могут решить проблему, но они заметно увеличивают сложность вашего кода и, фактически, они все равно менее эффективны чем совместное размещение состояний, так как Реакту нужно пройти через каждый компонент (начиная с верхнего), чтобы определить нужно ли рендерить его снова.


Если хотите поиграть с немного менее надуманным примером, переходите здесь на codesandbox.


Что такое совместное размещение состояний?


Принцип совместного размещения гласит:


Код должен быть расположен как можно ближе к тому месту к которому он имеет отношение.

Итак, что бы соответствовать этому принципу, наше dog состояние должно находиться внутри компонента DogName:


function DogName({time}) {
  const [dog, setDog] = React.useState('')
  return (
    <div>
      <label htmlFor="dog">Dog Name</label>
      <br />
      <input id="dog" value={dog} onChange={e => setDog(e.target.value)} />
      <p>
        {dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}
      </p>
    </div>
  )
}

Но что делать, если мы разобьем этот компонент на несколько компонентов? Куда должно передаваться состояние? Ответ все тот же: "как можно ближе к тому месту к которому он имеет отношение", а таковым будет ближайший общий родительский компонент. В качестве примера давайте разобьем компонент DogName, чтобы input и p отображались в разных компонентах:


function DogName({time}) {
  const [dog, setDog] = React.useState('')
  return (
    <div>
      <DogInput dog={dog} onChange={setDog} />
      <DogFavoriteNumberDisplay time={time} dog={dog} />
    </div>
  )
}

function DogInput({dog, onChange}) {
  return (
    <>
      <label htmlFor="dog">Dog Name</label>
      <br />
      <input id="dog" value={dog} onChange={e => onChange(e.target.value)} />
    </>
  )
}

function DogFavoriteNumberDisplay({time, dog}) {
  return (
    <p>
      {dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}
    </p>
  )
}

Мы не можем переместить состояние в компонент DogInput, так как компоненту DogFavoriteNumberDisplay тоже нужен доступ к состоянию, поэтому мы перемещаемся с низу в вверх по дереву компонентов, пока не найдем общий родительский элемент этих двух компонентов, и именно в нем организуем управление состоянием.


Все это относится и к ситуациям когда вам нужно управлять состоянием десятков компонентов на определенном экране вашего приложения. Вы даже можете переместить это в контекст (context), чтобы избежать prop drilling, если хотите. Но держите этот контекст как можно ближе к тому месту к которому он относится, и тогда вы продолжите поддерживать хорошую производительность (и удобство работы с кодом) которую дает совместное размещение. Помните, что вам не нужно размещать все контексты на верхнем уровне вашего Реакт приложения. Держите их там где в этом больше всего смысла.


Это основная мысль моего другого поста Application State Management with React. Держите ваши состояния как можно ближе к месту где они используются, это улучшит как производительность, так и удобство работы с кодом. При таком подходе, единственное что, возможно, будет ухудшать производительность вашего приложения это особенно сложные взаимодействия с элементами интерфейса.


Так что использовать, контексты или Redux?


Если вы читали "One simple trick to optimize React re-renders", то знаете что можно сделать так, чтобы обновлялись только те компоненты которые используют изменившееся состояние. Таким образом вы можете просто обойти эту проблему. Но люди все еще сталкиваются с проблемами производительности при использовании Редакса. Проблема в том, что React-Redux ожидает, что вы следуете его рекомендациям, чтобы избежать лишнего рендера подключенных компонентов. Всегда есть шанс ошибки, можно случайно настроить компоненты так, что они начнут слишком часто рендериться при изменении других глобальных состояний. И чем больше ваше приложение, тем сильнее будет негативный эффект от этого, особенно если вы добавляете в Редакс слишком много состояний.


Существуют методы которые помогут вам избежать этих проблем, к примеру используйте memoized Reselect селекторы для оптимизации фукций mapState, или почитайте документацию Редакса с дополнительной информацией о повышении производительности приложений.


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


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


Но факт остается фактом: совместное размещение состояний снижает проблемы производительности и упрощает обслуживание кода.


Принимаем решение о том куда поместить состояние


Дерево решений:


where-to-put-state


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


  • 1 Начинаем разработку приложения. Идем к 2
  • 2 Состояние в компоненте. Идем к 3
  • 3 Состояние используется только этим компонентом?
    • Да? Идем к 4
    • Нет? Это состояние нужно только одному дочернему компоненту?
    • Да? Перемещаем его в этот дочерний компонент (применяем совместное размещение). Идем к 3.
    • Нет? Это состояние нужно родительским или соседним ("братским" компонентам, то есть детям того же родительского компонента) компонентам?
      • Да? Перемещаем состояние выше, в родительский компонент. Идем к 3
      • Нет? Идем к 4
  • 4 Оставляем как есть. Идем к 5
  • 5 Есть ли проблема с "проп дриллингом"?
    • Да? Перемещаем это состояние в провайдер контекста и рендерим этот провайдер в компоненте в котором идет управление состоянием. Идем к 6
    • Нет? Идем к 6
  • 6 Отправляем приложение. При появлении новых требований, идем к 1

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


Заключение


В целом разработчики довольно хорошо понимают когда нужно поднимать состояние ("lifting state") когда это требуется, но мы не так хорошо понимаем когда состояние нужно опускать. Предлагаю вам взглянуть на код вашего приложения и подумать где состояние можно было бы опустить, применив принцип "совместного размещения". Спросите себя, "а нужно ли мне состояние isOpen модального окна в Редаксе?" (ответ, скорее всего, "нет").


Применяйте принцип совместного размещения, и ваш код станет проще и быстрее.


Удачи!

Теги:




Комментарии (55):

  1. alexesDev
    /#21194464

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


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

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

  2. roginvs
    /#21194650

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

    • serf
      /#21194728

      Нормальные фреймворки изначально дизайнились «от состояния (иммутабельного и реактивного)» и вот там не приходится потом год за годом добавлять «костыли» для твикинга работы со стейтом тк перформанс не радует. Реакт дизайнился полагая что dirty-checking ихнего vdom c dom это быстро/модно/молодежно и достаточно чтобы отбросить лишние отрисовки. Но с годами до них дошло что dirty-checking сама по себе не быстрая операция и кроме этого еще и vdom висит в памяти что для для мобилок или слабых ПК тоже не очень хорошо. И вот теперь добавляют свои useMemo и прочие хелперы работы со стейтом когда во взрослых фреймворках это изначально сделано. В итоге из изначально простой библиотеки реакт все больше становится костыльной поделкой.

    • IvanGanev
      /#21194740

      Описанный в статьей метод прост в применении и не требует ничего кроме самого реакта.

      • markelov69
        /#21195480

        Сам по себе голый реакт это фигня, а вот MobX превращает его в отличную вещь. Посмотрите это habr.com/ru/post/485032/#comment_21195466

        • IvanGanev
          /#21195922

          Я знаю про MobX и да же работаю с ним. Но это не имеет никакого отношения к статье. Одно дело провести рефакторинг приложения опустив нужные стейты по иерархии компонентов, и совсем другое переводить весь проект на новый стейт менеджер.

          • markelov69
            /#21197344

            Перевести на MobX как раз таки не проблема

  3. MaZaAa
    /#21194674

    MobX? Не, не слышал… В очередной раз… ппц…

  4. DarthVictor
    /#21194774

    Не будет ReactRedux перерендеривать SlowComponent при использовании в контейнере только свойств «dog» и «onChange». По крайней мере при использовании connect с дефолтными настройками.
    react-redux.js.org/api/connect#pure-boolean

  5. JustDont
    /#21195160

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

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

    • markelov69
      /#21195466

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

      И вот голый React с его неадекватными рендерами всего и вся. codesandbox.io/s/romantic-hermann-ts6ny

      • JustDont
        /#21195478

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

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

  6. nohuhu
    /#21196154

    Так что использовать, контексты или Redux?

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


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

    • JustDont
      /#21196838

      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 тем, чему в ней явно не место — это сильный моветон.

      • nohuhu
        /#21201226

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


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

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


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


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

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

        • JustDont
          /#21201264

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

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

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

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

          • nohuhu
            /#21201604

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

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


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

            Ага, вот тут уже понятнее. Если честно, я с вами вполне согласен — 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 вот здесь, а он нужен ещё вон там в соседнем компоненте, решается банально переносом одного свойства в родительскую модель. И всё просто работает. :)

    • markelov69
      /#21197370

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

      • nohuhu
        /#21201282

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

        • markelov69
          /#21201318

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

          • nohuhu
            /#21201790

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


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

            • markelov69
              /#21202854

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

              • nohuhu
                /#21207018

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


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


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


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

                • JustDont
                  /#21207066

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

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

                  • nohuhu
                    /#21207086

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


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

                    • JustDont
                      /#21207108

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

                      Куда угодно.

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

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

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

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

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

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

                      • nohuhu
                        /#21207166

                        Куда угодно.

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


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

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


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


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


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

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


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

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


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

                        • JustDont
                          /#21207220

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

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

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

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

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

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

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

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

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

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

                          • nohuhu
                            /#21207424

                            Ну 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 минуты, это я опечатываюсь много.

                            • JustDont
                              /#21209038

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                              • nohuhu
                                /#21212460

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

                                Отлично, мы завели переменную в компоненте. Что это даёт? Ничего, всё равно надо подписываться на обновления вручную. Вот как в примере 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? Так мне оно не нужно (что бы это ни было), мне нужно форму с тремя полями. Или таблицу с фильтрацией.

                                • markelov69
                                  /#21212818

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

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

                • markelov69
                  /#21208814

                  Раз вы такое пишете, значит вы вообще не копали MobX и даже не пробовали его в деле

                • markelov69
                  /#21208914

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

                  • nohuhu
                    /#21212202

                    Раз вы такое пишете, значит вы вообще не копали MobX и даже не пробовали его в деле

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


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

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

                    • markelov69
                      /#21212812

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

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

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

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

                      • nohuhu
                        /#21213026

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


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

                        • JustDont
                          /#21213786

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

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

                        • markelov69
                          /#21214152

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

        • kahi4
          /#21207150

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


          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, не знаю что поломалось.



          • nohuhu
            /#21207360

            Спасибо за критику! Нет, правда, мне очень интересны критические/негативные отзывы. Это возможность узнать о проблемах и быстрее их решить. :) Текущая версия библиотеки всего 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 в тестировании уже более полугода, полёт номинальный. Скоро релиз. :)

            • kahi4
              /#21207420

              Объединить оба компонента под родительской 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, который хранит список предметов. А теперь вам нужно эту оценку для каждого ученика в левой панели показать — опять все всплывает вверх. А потом среднюю по всем ученикам… И динамику, заодно.

              • nohuhu
                /#21207526

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

                Навскидку, странный общий ключ у общего родителя и в 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 не очень, поэтому обычно все и говорят, что она не нужна. :)

            • kahi4
              /#21207430

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

              • nohuhu
                /#21207536

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


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

  7. noodles
    /#21207094

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

    Давно интересуюсь, а какие недостатки держать все контексты на самом верху, что плохого? Память? Производительность?
    Типа вот так:
    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.

    • IvanGanev
      /#21212458

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

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

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

      • noodles
        /#21216494

        мы все держим «ближе к телу»

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