Возможно ли без Redux?
На сегодняшний день можно найти уйму позиций, где требуется 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