На сегодняшний день можно найти уйму позиций, где требуется react/redux. React прекрасен, вопросов нет. Вопрос к Redux — возможно ли без него. Если погуглить чуть-чуть, найдется добротная статья на Хабре, где автор задается таким же вопросом. В статье на простом примере (todoList) метод this.updateViews() вызывается слишком часто (семь-восемь раз) и кажется, что можно сделать проще.

Основная идея тут observable models, react отвечает за observable, дело осталось за малым — создать model.

Перед созданием модели пару слов о дизайне (архитектуре) клиента:

index — raw data
history — array[model]
observer — model
view — errors, focus, flags

index.jsx — точка входа программы для экрана пользователя. Index отрисовывает все компоненты с данными по умолчанию, делает асинхронные запросы, перерисовывает компоненты с новыми данными.

// index.jsx

<History>
  <Observer>
     <HeaderView />
     <MainView />
  </Observer>
</History>

Observer.jsx отвечает за синхронизацию модели для нескольких views. Например, Петя заполняет форму для оффера и в шапке страницы видит real-time preview. Observer хранит объект модели, предоставляя дочерним компонентам api: onModelChange(field, value).

History.jsx — это stack объектов модели, где api: commit и rollback.

Model.js — это то, что пользователь может ввести ручками,— то есть самое ценное. Другие данные в модели хранить не нужно. Model — это не react компонент, а обычный js class.

class Model {
  constructor(other = {}) {} // copy constructor (and default too)
  isEqual(other) {} // operator ==
  less(other) {} // operator< 

  swap(other) {}
  hash() {}
  fieldNameConstrains() {} //see below please 
}

Конструктор копирования как минимум нужен для History. Метод isEqual — для popup-unsaved-changes (что гораздо удобнее флага в state). Метод fieldNameConstrains — для зависимых полей.

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

class Model { 
   // constrains
   // distance  <== velocity * time

   velocityConstrains(newVelocity) {
     this.velocity = newVelocity;
     this.distance = this.velocity * this.time; 
   }
   timeConstrains(newTime) { … } 

   distanceConstrains(newDistance) {
     this.distance = newDistance;
     this.time = this.distance / this.velocity;   
   }
}

По личному опыту, что-то типа model.field.onchange не работает, так как иногда нужно вызвать конструктор копирования и onchange события совсем не нужны.

View.jsx

class View extends React.Component {
  state = {
    errors: {},
    focus: {},
    …
  }

  render() {    
   …
    <input 
       value={this.props.model.title} 
       onChange={e => this.props.onModelChange(‘title’, e.target.value)} />
   …
  } 
}

Валидация. Валидацию не нужно делать в модели. Ее нужно проводить во view (не забываем, что view это экран пользователя и что на экране может быть показана не вся модель). Валидаторы это набор предикатов. Для валидации есть всего два алгоритма: 1) находим все ошибки в форме или 2) находим первую ошибку. Например,

class View extends React.Component {

onClickSaveButton() {
  const mapper =  {
    title: () => model.title.length && !maxLenValidator(model.title, 25),
    price: () => !(model.price % 40 == 0),
    url: () => !urlValidator(model.url),
    …
  }
  const errors = map(mapper, (validator, key) => {
    return validator() ? key : undefined;       
  }).filter(Boolean);

}

// валидаторы легко тестировать и легко переиспользовать

Права доступа. Тут главное удержаться и не использовать наследование. Идея такая, что модель содержит все поля и мы урезаем поля под роли. То есть это whitelist, остальные поля в модели остаются по умолчанию. Для валидации добавляется один шаг — делаем проекцию объекта валидации (он же mapper, см. выше), то есть валидируем только нужные поля.

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

O методе модели isEqual. Где-нибудь в utils.js будут методы isEqual и isEqualArray:

  function isEqual(left, right) {
     return left.isEqual(right); 
  }
  isEqualArray(v1, v2) {
    if (v1.length !== v2.length) { return false }
    for (var k = 0; k != v1.length; k++) {
      if (!isEqual(v1[k], v2[k])) { return false; }
    }
    return true;   
  }   

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

Ссылки:

Раз
> Два

Комментарии (39)


  1. FCron
    06.06.2019 23:55

    отвечая на вопрос in subject — возможно. надо просто смотреть шире


  1. zompin
    07.06.2019 00:01

    Можете написать свое хранилище на стейте и конекстах)


  1. vanxant
    07.06.2019 01:20
    +3

    Вот сейчас хапну пачку минусов, но реакт со своими игрищами находится примерно там же, где и php году так в 2005. Flux/redux store — это прямой аналог $_GLOBALS, т.е. с точки зрения ООП — самый что ни на есть God object в наихудшем своём виде, когда он by design отвечает вообще за всё приложение. Модульность? Не, не слышал.
    Потом, когда оказалось, что под капотом специально для redux в самом react есть контексты, даже если вы не используете redux, и скрывать этот факт стало совсем уж неприлично — контексты документировали. Т.е. открыли для всех аналог global-объявлений.
    При этом родной для javascript способ хранения состояния объектов — ну, в самих объектах, считается фу-фу-фу и грязь.
    Зато завезли хуки — сегодняшняя их версия выглядит нестрашно, но их нужно давить пока маленькие. Хуки приводят к самому дикому лапшекоду, который только можно представить, т.к. по сути представляют собой хорошо замаскированный goto. Посмотрите на сегодняшний вордпресс и особенно его интернет-магазин woocommerce.
    Вместо того, чтобы выносить разработчикам мозг и заставлять их использовать одну конкретную архитектуру приложения, Абрамову и компании не помешало бы оглянуться вокруг и привести своё детище к современным стандартам. Баг про несовместимость реакта со стандартными модулями висит третий год — т.е. вы можете загрузить в браузер что угодно через script type=«module», включая подавляющее большинство компонентов для реакта, но не сам реакт. Уродский и сломанный во всех местах JSX вместе с jsx-компилятором давно пора закопать в пользу нативных js-шаблонов и template — как минимум, браузеры обрабатывают это быстрее. React-dom нужно уравнять по функционалу хотя бы с пятилетней свежести jquery, чтобы последний не тащили зависимостью в каждом первом проекте. Мечты, мечты…


    1. dagen
      07.06.2019 01:57

      Раз уж такая пьянка, то тоже хапну пару минусов.


      Потом, когда оказалось, что под капотом специально для redux в самом react есть контексты, и скрывать этот факт стало совсем уж неприлично — контексты документировали.

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


    1. mayorovp
      07.06.2019 09:20
      +1

      При этом родной для javascript способ хранения состояния объектов — ну, в самих объектах, считается фу-фу-фу и грязь.

      Хранить-то их там можно без проблем, вопрос лишь в том как вовремя замечать в них изменения? Тут нужно либо использовать протокол "сквозная иммутабельность + не создаем лишний раз новых объектов" (и тогда в пределе получится redux), либо использовать явные подписки на изменения (и тогда в пределе получится rx.js), либо использовать автоматические подписки (и тогда в пределе получится mobx). Все три способа возможны и реактом не запрещаются, а других способов нет.


      Уродский и сломанный во всех местах JSX вместе с jsx-компилятором давно пора закопать в пользу нативных js-шаблонов и template — как минимум, браузеры обрабатывают это быстрее.

      Это невозможно сделать сохраняя основную идею реакта (метод render). Точнее, возможно — но будет медленнее, а не быстрее.


      React-dom нужно уравнять по функционалу хотя бы с пятилетней свежести jquery

      Например?..


      1. vanxant
        07.06.2019 13:09

        а других способов нет

        Computable и Observable поля были ещё в knockout.js и самом-самом первом angular.js, существовавших до реакта.
        Особенно смешно про иммутабельность. Примерно все умеют в Object.freeze, который как раз про иммутабельность, кроме react.js.
        Это невозможно сделать сохраняя основную идею реакта (метод render). Точнее, возможно — но будет медленнее, а не быстрее.

        HTML тэг template парсится один раз при загрузке браузером. То же самое, гипотетически, возможно с `литералами`. Дальше они клонируются, а не создаются. Это тот самый виртуальный дом, только реализованный движком браузера и быстро, а не через вызовы CreateElement.
        Например?..

        Ну например один раз при старте приложения повесить один обработчик onChange на все поля ввода определённого класса, а не вешать свой handleChange на каждое поле при создании, а потом уничтожать при удалении из DOM.


        1. Odrin
          07.06.2019 14:59

          Ну например один раз при старте приложения повесить один обработчик onChange на все поля ввода определённого класса
          Зачем пихать в react то, что решается одной строчкой кода?
          if (!e.target.classList.contains(cls)) return;


        1. mayorovp
          07.06.2019 17:07
          +1

          Computable и Observable поля были ещё в knockout.js и самом-самом первом angular.js, существовавших до реакта.

          Ну так идейным наследником knockout как раз mobx и является.


          Особенно смешно про иммутабельность. Примерно все умеют в Object.freeze, который как раз про иммутабельность, кроме react.js.

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


          HTML тэг template парсится один раз при загрузке браузером. То же самое, гипотетически, возможно с литералами. Дальше они клонируются, а не создаются. Это тот самый виртуальный дом, только реализованный движком браузера и быстро, а не через вызовы CreateElement.

          А дальше что? Как обновлять результат такого "рендера"?


    1. xyli0o Автор
      07.06.2019 09:30

      в пользу нативных js-шаблонов

      Вы имеете ввиду developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals?

      PS: спасибо за коммент


    1. alexesDev
      07.06.2019 11:04
      +1

      Со всем не согласен =)


      Хуки в React удобны (раза в два короче классов, лучше читать лапшекод с хуками, чем лапшекод с классами), только построены на той самой $_GLOBALS к сожалению для того, чтобы не ломать код. Если бы поменяли api на что-то вроде


      const MyComponent = (props, io) => {
        const [value, setValue] = useState(io, 'default value');
      };

      стало бы лучше, это ЯВНЫЙ способ закрепить что-то на узле virtual dom. Сейчас этот io — объект в react модуле (сразу лазил смотреть, как сделали). Тестировать не удобно и тп.


      Контекст нормальный и это не GLOBALS, потому что контекст доступен только в поддереве. Удобный механизм прокидываний props глубоко внутрь (не нравится — кидайте руками и получите по 100500 props на верхнем уровне).


      JSX тоже нормальный, если целиком понимать, что внутри это React.createElement(MyComponent, myProps), просто сахар. И я хотел бы такой сахар например в c++ когда нужно делать подобные деревья (scenegraph в играх). К сожалению большая часть народа, которая хочет зп до 150к не понимает этого вообще (мой опыт).


    1. DarthVictor
      07.06.2019 11:33
      +1

      Flux/redux store — это прямой аналог $_GLOBALS, т.е. с точки зрения ООП — самый что ни на есть God object в наихудшем своём виде, когда он by design отвечает вообще за всё приложение.

      А вы не могли бы предложить альтернативный способ построения приложений, чтобы можно было более предментно обсуждать?


      1. vanxant
        07.06.2019 13:03

        MVVM как в конкурирующих фреймворках, например.


        1. DarthVictor
          07.06.2019 13:32

          А как MVVM регулирует взаимодействие разделяемого несколькими компонентами состояние модели? Потому что без него и в React отдельный менеджер состояний не нужен. Хватит аккуратного разделения на умные компоненты со стейтом и глупые с JSX, это даже линтером наверное можно настроить.


          1. vanxant
            07.06.2019 14:25

            Прелесть MVVM в том, что у вас «из коробки» отношение многие-ко-многим между моделями и вьюхами. Надо вам несколько вьюх к одной модели — да пожалуйста, как и наоборот. Всю «магию» по разруливанию связей берёт на себя слой ViewModel, и да, да, двустороннее связывание.
            С JSX, точнее redux, у вас дерево и одностороннее связывание. И довольно часто ближайшим общим предком компонентов, которым нужно общее состояние, является корневой объект. Для компонентов с собственным состоянием начинаются пляски с бубном, все эти UNSAFE_componentWillReceiveProps и попытки определить, кто именно вызвал изменения пропсов — сервер, сам компонент или что-то другое, чтобы не уйти в бесконечный цикл. А ещё такая схема часто вызывает перерендеринг всего, т.к. в реакте мелкое сравнение на изменение пропсов. И не надо рассказывать, что это быстро.


            1. DarthVictor
              07.06.2019 16:12
              +1

              Я не уверен, что правильно понял, но чем это отличается от Redux, где модели — это ветки стора с редьюсерами, контейнеры вместе с connect, mapStateToProps и mapDispatchToProps — это ViewModel, а глупые компоненты — View?

              определить, кто именно вызвал изменения пропсов — сервер, сам компонент или что-то другое

              С точки зрения менеджера состояний — должно быть ясно из названия вызванного экшена или просто из того в какой функции модели вы находитесь. С точки зрения стандартных глупых компонентов — должно быть пофигу. Специально вводили виртуальный DOM чтобы не заниматься обходом всех пропсов и смотреть какой кусок DOM менять, как бывало в первом AngularJS. Либо ссылки на пропсы поменялись и обновляем всё, либо — нет, и мы ничего не трогаем.


        1. mayorovp
          07.06.2019 17:10
          +2

          Не вижу связи между глобальными переменными и MVVM. Модель-то нужно точно так же откуда-то получать, что можно сделать теми же самыми способами которыми можно получить стор.


    1. Zibx
      07.06.2019 13:35

      Jsx — это всего лишь сахар. Лично люблю его изнасиловать и подсунуть ему свою h функцию, после чего получить <span/> = HTMLElement с детями и без необходимости руками проделывать рутинные операции по сборке дерева.


      1. vanxant
        07.06.2019 14:31

        Прикол в том, что кроме JSX в голом реакте есть ещё ровно два класса — react и reactComponent :) Т.е. сам по себе react.js пустой до неприличия


  1. Hwd
    07.06.2019 08:24
    +1

    Можно ли жить без ног? Можно, но с ногами удобней. Если они есть, то отказываться от них глупо. Тогда уж отказывайся и от рук, чтобы "свобода" была полной. Имхо.


  1. diffusion
    07.06.2019 09:01
    +1

    Так то MobX есть, как альтернатива ридаксу. Он то как раз про observable и observers и не принуждает к конкретной архитектуре. Просто удобный способ, связать данные в стейте и компоненты


    1. xyli0o Автор
      07.06.2019 09:16
      +1

      Mobx хорош. Очень небольшой его минус: 1) изменяет view, добавляя декоратор и 2) если обновляются k зависимых полей, метод render вызывается k раз (что не проблема для react)


      1. mayorovp
        07.06.2019 09:22
        +1

        изменяет view, добавляя декоратор

        Но ведь вам в любом случае нужно что-то менять во view, когда вы меняете интерфейс модели...


        если обновляются k зависимых полей, метод render вызывается k раз (что не проблема для react)

        Давно уже придуман action, который решает именно эту проблему


        1. xyli0o Автор
          07.06.2019 10:01

          Component {
            props.model
            props.onModelChange(fieldName, value)
          }
          


          @observer
          Component {
            props.model
          }
          


          Дело вкуса.
          Не нравится, что во втором варианте любой (не имеющий отношения к модели) observable props будет форсить компонент к перерисовке.


          1. mayorovp
            07.06.2019 10:11
            +1

            А откуда у вас в нормальной программе возьмется "не имеющий отношения к модели observable props"? И что, по вашему, должен сделать правильный компонент, когда используемая им при рендере информация изменилась? Неужели проигнорировать это изменение?


            Кстати, информация о fieldName — бесполезная. Реакт просто не умеет обновлять компонент по частям, метод render() можно вызвать лишь целиком.


            1. xyli0o Автор
              07.06.2019 11:27

              Хмм… MobX отличает observable поля state и props от других observable полей объекта (e.g. this._counter)

              информация о fieldName — полезная. onModelChange обычный callback в child-parent communication


              1. mayorovp
                07.06.2019 11:34

                Ну да, отличает. Но ведь при любом изменении props Реакт и без mobx перерисует компонент (если это PureComponent или Component без shouldComponentUpdate), так в чем отличие именно mobx?


                1. xyli0o Автор
                  07.06.2019 11:50

                  Все верно, вы правы.

                  У меня была ошибочная гипотеза:

                  @observer
                  class Component {
                    @observable _counter = 0;
                    
                    someFunction() {
                     if (nothingChange) {
                        this._counter++; //  I thought that it should force rerender 
                        return;
                     }
                     ...
                    }
                  }
                  


      1. apapacy
        07.06.2019 09:34

        Я сам всегда примерно redux и недавно попался чужой проект на monx после чего скорее всего буду с ним работать. Но redux ведь тоже как минимум оборачивает в декоратор connect компонент react


        1. xyli0o Автор
          07.06.2019 10:03
          +1

          Лучше без redux :)


      1. ilyapirogov
        07.06.2019 16:25

        mobx-react-lite не меняет view. https://mobx-react.netlify.com/observer-component


        1. mayorovp
          07.06.2019 16:47

          Это теперь называется "не меняет"? :-)


          1. ilyapirogov
            07.06.2019 17:00

            Разумеется, сам рендер меняется. Так же как и при использовании нативных контестов, например. Я имел ввиду, что он не добавляет никаких магических декораторов. Это более прозрачный подход и позволяет следить за локальными изменениями в дереве, а не всем компонентам.


            1. mayorovp
              07.06.2019 17:12

              Ну да, магических декораторов нет, только теперь есть ещё более магический хук.


              Observer component, к слову, в mobx-react был примерно всегда.


              1. ilyapirogov
                07.06.2019 17:39

                А при чем здесь хук? Про него речи вообще не было. Никто им не заставляет пользоваться.


                1. mayorovp
                  07.06.2019 19:19
                  +1

                  При том, что поддержка хуков — единственное что появилось в mobx-react-lite по сравнению со старым mobx-react.


  1. worldxaker
    07.06.2019 10:49

    попробуйте effector


  1. abramov231
    07.06.2019 17:05
    +5

    Не только возможно, а гораздо лучше без Redux. Я переводил с Redux на MobX два приложения в Яндекс и UBS — субъективно скорость разработки выросла раза в три за счёт избавления от бойлерплейта. И самое главное из за чего мы инвестировали уйму времени на миграцию на втором проекте — производительность UI значительно выросла и вернулась к O(n), где n — число компонентов для рендеринга. В Redux каждый reducer обрабатывает каждый action, что даёт квадратичный рост времени вычислений. То что Redux подходит для больших приложений это не правда, так как в действительно больших приложениях у вас вырастают требования к быстродействию каждого компонента. Можно перепесать механизм композиции редюсеров и избавиться от сложности O(n ^ 2), но производительность продолжить есть копиривание объектов для достижения иммутабельности. Кроме того, Redux нарушает фрактальную природу программ, когда части имеют ту же структуру, что и целое, предлагая глобальный стэйт без возможности инкапсуляции. Redux хорошо распиарен, но я не рекомендую исспользовать его в серьезных проектах, так как количество архитектурных багов там слишком велико. Вы их победите конечно, но потеряете время.


    1. iKBAHT
      07.06.2019 20:58
      +3

      Полностью согласен с


      не рекомендую использовать его в серьезных проектах

      Все дело в инкапсуляции. На мой взгляд это самый серьезный недостаток Redux для крупных проектов. Store один, поэтому


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

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


      скорость разработки выросла раза в три за счёт избавления от бойлерплейта


    1. faiwer
      07.06.2019 23:24
      +1

      В Redux каждый reducer обрабатывает каждый action

      Это зависит от того как вы ваш редьюсер приготовите. Совершенно не обязательно писать его "классически". Redux не навязывает вам того как редьюсер должен быть устроен. Лично я предпочитаю 1 action = 1 handler, а сам reducer как древо hash-map вида [action.type piece]: handler. В таком случае никаких O(n), никаких switch-case.


      есть копиривание объектов для достижения иммутабельности

      А тут асимптотика равна глубине древа. Т.е. совсем чуть-чуть.


      Проблема с производительностью в redux кроется скорее в том, что react-redux-ий connect написан так, что все его instance-ы вызываются на любое изменение store. Это ограничение преодолимо, за счёт написания своего иерархического HoC, вместо react-redux.


      Ну и в целом сделать большое приложение с иммутабельным древом производительным стоит кучи геморроя в поддержке кеширования промежуточных вычислений, обеспечения бесперебойной работы React.memo | PureComponent. Это мягко говоря не тривиально и пестрит кучей нюансов. Мало кто умеет это делать правильно. Хуже того — мало кто пытался пробовать.