Возможно ли без Redux?

habr.png

На сегодняшний день можно найти уйму позиций, где требуется react/redux. React прекрасен, вопросов нет. Вопрос к Redux — возможно ли без него. Если погуглить чуть-чуть, найдется добротная статья на хабре (https://habr.com/ru/post/350850/), где автор задается таким же вопросом. В статье на простом примере (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


  
     
     
  


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() {    
   …
     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;   
  }   

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

Ссылки:
parasol.tamu.edu/~jarvi/papers/gpce08.pdf
stlab.cc/tips/about-mvc.html

© Habrahabr.ru