В этой статье я бы хотел подискутировать о том, насколько хорошо паттерн MVVM подходит для разработки приложений на React. Вместе этим, я собираюсь описать какие преимущества могут быть при разработке с использованием MobX и паттерна MVVM в сравнении с Redux. Запаситесь кофе, это будет долгое чтиво.
Вообще если проанализировать статьи по сравнению MobX и Redux, можно заметить несколько часто повторяющихся тезисов:
"MobX проще в изучении, чем Redux"
"MobX более производительнее Redux"
"В MobX нужно писать меньше кода, чем в Redux"
Однако, "Масштабирование на Redux проще, чем на MobX", а потому "MobX подходит для небольших приложений, а Redux для больших"
"Процесс дебага на Redux проще, чем на MobX"
"Сообщество Redux обширнее, чем у MobX"
"В MobX дается слишком много свободы"
Чтобы не быть голословным для примера приложу несколько статей
На самом деле первые 4 статьи написаны будто под копирку и подобных статей очень много.
Глупо отрицать, что в сравнении с Redux сообщество MobX не такое большое - одних только скачивай на npmjs.com у Redux больше в 8 раз чем у MobX. Однако, с остальными минусами я с трудом могу согласиться.
Проблемы с дебагом, как мне кажется, либо являются исключительно субъективными, либо появляются в следствие плохой архитектуры приложения. Сам же я, например, никогда проблем с дебагом MobX не испытывал.
Поинт про масштабирование в моих глазах тоже выглядит странно. Чем больше становится приложение на Redux, чем больше в нем селекторов, больше логики в редьюсерах, больше мидлварей, тем медленней он становится. Как бы не был оптимизирован код на Redux, при масштабировании производительность будет обязательно падать. MobX же в плане производительности может расширяться довольно безболезненно. Поэтому я предполагаю, что авторы таких статей говорят о проблемах в масштабировании в контексте написания нового кода. В таком случае это снова является проблемой плохой архитектуры.
И вот мы добрались до сладкого. Я не просто так выделил проблему излишней свободы в MobX. Ведь она вполне реальна. Создавай сторы как угодно; используй их в компоненте, предварительно импортируя, используй их с помощью инъекции или создавай их в компоненте. Подходов к использованию MobX в React можно придумать невероятно много. И MobX даже не пытается рекомендовать какой-то один "хороший" подход. А в совокупности с тем, что его "легко изучить", эта проблема только усугубляется, так как, быстро выучив основные концепции MobX, некоторые разработчики считают, что они определенно точно знают, как нужно продумывать архитектуру.
Но все-таки проблема архитектуры приложения не связана с MobX напрямую. Странно обвинять пистолет, если ты сам выстрелил себе в ногу. Проблема в том, что разработчики не выбрали какой-то определенный подход к использованию этого инструмента. А ведь прорабатывать подход с нуля вовсе не обязательно - существующие концепции неплохо справляются с задачей по проработке архитектуры. И как вы наверняка догадались, в рамках данной статьи я попытаюсь показать, как паттерн MVVM может в достаточной степени сформировать ту строгость, о недостатке которой говорится в подобных статьях.
Для этой статьи я написал небольшую библиотеку, с помощью которой можно было бы использовать паттерн MVVM. Она состоит буквально из 3 функций и 2 классов. Далее по ходу статьи я буду часто к ней обращаться.
Что такое паттерн MVVM?
Основное преимущество паттерна MVVM (Model View ViewModel) заключается в разделении разработки графического интерфейса и логики.
Взгляните на короткий пример
import { action, observable, makeObservable } from 'mobx';
import { view, ViewModel } from '@yoskutik/react-vvm';
class CounterViewModel extends ViewModel {
@observalbe count = 0;
constructor() {
super();
makeObservable(this);
}
@action increate = () => {
this.count++;
};
}
const Counter = view(CounterViewModel)(({ viewModel }) => (
<div>
<span>Counter: {viewModel.count}</span>
<button onClick={() => viewModel.increate()}>increase</button>
</div>
));
И я говорю о полном делении логики и отображения. В подходе MVVM ваши умные компоненты могут состоять исключительно из JSX кода. Таким коротким примером я всего лишь хотел вас заинтересовать. Но давайте не забегать вперед, и сначала обозначим, как можно использовать MVVM в React-приложениях.
MVVM в контексте React-приложений
Невозможно описать все плюсы MVVM, не рассказав об особенностях реализации этого паттерна в React-приложениях. Для себя я выделил всего 4 правила в реализации MVVM:
Каждая вьюмодель должна быть привязана ко вью и, следовательно, при удалении вью из разметки вьюмодель должна "умирать";
Каждая вьюмодель может (и зачастую будет) являться стором;
Этот стор может использоваться не только в компоненте, который создал вьюмодель (т.е. внутри вью), но и во всех дочерних компонентах вью;
Вью и вьюмодели знают о существовании друг друга.
В чем заключается осведомленность вью и вьюмодели друг о друге? В React приложении вью создаст сам React. А вот за создание вьюмодели уже будет ответственен вью. В свою очередь вьюмодель знает о пропсах вью и способна обрабатывать различную логику на разных этапах жизненного цикла вью.
А теперь о преимуществах
Наверняка, вы задались вопросом: "Но почему вьюмодель должна умирать, когда вью уходит из разметки?". Ответ довольно прост. Если компонент более не находится в виртуальном DOM'е, с большой вероятностью необходимости в хранении данных для отображения этого компонента нет. Эта оптимизация позволяет освобождать память приложения, когда некоторые куски данных не используются.
И это лишь одно из немногих преимуществ.
Context API и абстракция данных
Третье "правило" моей реализации говорит, что вьюмодель может использоваться на любой глубине внутри вью. Понятно, что речь идет о Context API. И его вполне можно использовать вместе с паттерном MVVM.
Использование MVVM вместe c Context API
import { makeObservable, observable } from 'mobx';
import { childView, view, ViewModel } from '@yoskutik/react-vvm';
class SomeViewModel extends ViewModel {
@observable field1 = '';
constructor() {
super();
makeObservable(this);
}
doSomething = () => {};
}
// ChildView не создает вьюмодель и должен быть расположен где-то внутри
// View. Таким образом он сможет взаимодействовать с SomeViewModel.
const ChildView = childView<SomeViewModel>()(({ viewModel }) => (
<div>
<span>{viewModel.field1}</span>
<button onClick={viewModel.doSomething}>Click in a child of view</button>
</div>
));
const View = view(SomeViewModel)(({ viewModel }) => (
<div>
<span>{viewModel.field1}</span>
<button onClick={viewModel.doSomething}>Click in a view</button>
<ChildView />
</div>
));
По мне так тут прослеживается довольно четкая аналогия с Redux, который исключительно на Context API и существует. Однако, между подходом Redux и MobX с MVVM есть существенная разница.
Взгляните на эти схемы
Каждый узел в графе на схеме является React-компонентом.
В подходе Redux создается один стор. И этот стор доступен во всем приложении. В подходе MVVM стор создается только для вью. И в данном случае создается всего 3 стора. При этом синий стор и компоненты, его использующие, не знают о существовании желтого и красного стора, они никак не могут прочитать их данные или как-то на них повлиять.
В отличии от Redux, в MVVM появляется реальная абстракция данных. Например, данные, необходимые одной странице, могут быть видимы только внутри этой страницы, а то, что нужно модальному окну, будет видно только ему.
Вложенные вью
Сейчас вы могли увидеть некоторое противоречие. Я говорил, что вьюмодель должна быть доступна для любых детей вью, а на схеме ясно видно, что красные узлы имеют собственную вью модель, но никак не "желтую".
На такой случай было введено дополнительное понятие - родительская вьюмодель. Родительской вьюмоделью для "красной" будет являться "желтая".
А в коде использование родительской вьюмодели может выглядеть так
import { view, ViewModel } from '@yoskutik/react-vvm';
class ViewModel1 extends ViewModel {
doSomething = () => {};
}
class ViewModel2 extends ViewModel<ViewModel1> {
onClick = () => {
this.parent.doSomething();
};
}
const View2 = view(ViewModel2)(({ viewModel }) => (
<button onClick={viewModel.onClick} />
));
const View1 = view(ViewModel1)(({ viewModel }) => (
<div>
<View2 />
</div>
));
Но интересен также тот факт, что родительской вьюмоделью может быть даже интерфейс. Если некоторое вью может использоваться внутри разных вью, вьюмодели которых реализуют некоторый единый интерфейс, то в качестве типа родительской вьюмодели можно его и указать.
Пример использования родительской вьюмодели с типизацией в виде интерфейса
import { view, ViewModel } from '@yoskutik/react-vvm';
import { ISomeViewModel } from './ISomeViewModel';
class ViewModel1 extends ViewModel implements ISomeViewModel { ... }
class ViewModel2 extends ViewModel implements ISomeViewModel { ... }
// Тип родительской вьюмодели будет ISomeViewModel
class ViewModel3 extends ViewModel<ISomeViewModel> { ... }
const View3 = view(ViewModel3)(({ viewModel }) => (
<div />
));
// View3 может использоваться в разных view
const View1 = view(ViewModel1)(({ viewModel }) => (
<View3 />
));
const View2 = view(ViewModel2)(({ viewModel }) => (
<View3 />
));
Звучит, наверное, сложно. Но на практике это не так. Например, допустим в проекте есть некоторый виджет, который может появляться на нескольких страницах, и при этом зависеть одинаковым образом от этих страниц. У всех страниц будут свои вью модели, но правила отображения виджета будут одни для каждой страницы. И эти правила как раз можно было бы обозначить интерфейсом родительской вьюмодели.
Полное разделение логики и отображения
Как я написал выше, в MVVM сепарация логики и отображения может выйти на новый уровень. Мне невероятно сильно нравится концепция того, что умные компоненты могут состоять исключительно из JSX кода. По мне так это сильно упрощает анализ React компонент.
Создание обработчиков во вьюмодели
Начнем с простого. Обработчики событий по типу onClick
, onInput
и т.п. по определению MVVM можно определять во вьюмодели. В самом первом примере во вьюмодели я создал функцию increase
, которую далее вызвал в компоненте. Но мне ничего не мешало бы назвать функцию как onClick
, тогда в компоненте можно было бы использовать её напрямую
const Counter = view(CounterViewModel)(({ viewModel }) => (
<div>
<span>Counter: {viewModel.count}</span>
<button onClick={viewModel.onClick}>increase</button>
</div>
));
Кстати, у такого объявления обработчиков есть приятный эффект - все функции внутри вьюмодели, можно сказать, мемоизированы, т.е. они не меняются от рендера к рендеру вью. А значит не нужно беспокоиться об использовании useCallback
и других подобных хуков при создании обработчиков во вьюмодели.
Вьюмодель знает о пропсах вью
Даже если обработчики событий как-то зависят от пропсов вью, их все равно можно объявлять внутри вьюмодели, т.к. вьюмодель знает о пропсах вью.
Пример использования пропсов в обработчиках событий
import { FC } from 'react';
import { view, ViewModel } from '@yoskutik/react-vvm';
type WindowProps = {
title: string;
onClose: () => void;
}
class WindowViewModel extends ViewModel<unknown, WindowProps> {
onCloseClick = () => {
// do something else
this.viewProps.onClose();
};
}
export const Window: FC<WindowProps> = view(WindowViewModel)(({ viewModel, title }) => (
<div className="window">
<h1>{title}</h1>
<button onClick={viewModel.onCloseClick}>close</button>
</div>
));
А ещё мне показалось логичным сделать поле viewProps
наблюдаемым (observable). Таким образом можно создавать реакции на изменение определенных пропсов. И такие реакции по мне гораздо проще читать нежели реакции, создаваемые в useEffect
. Хотя, вероятно, это субъективное мнение.
Пример реакции с useEffect
import { FC, useCallback, useEffect } from 'react';
type WindowProps = {
title: string;
state: 'warn' | 'error';
onClose: () => void;
}
export const Window: FC<WindowProps> = ({ title, state, onClose }) => {
const onCloseClick = useCallback(() => {
// do something
onClose();
}, [onClose]);
useEffect(() => {
console.log(state);
}, [state]);
return (
<div className={`window window--${state}`}>
<h1>{title}</h1>
<button onClick={viewModel.onCloseClick}>close</button>
</div>
);
};
Пример реакции в MVVM
import { FC } from 'react';
import { view, ViewModel } from '@yoskutik/react-vvm';
type WindowProps = {
title: string;
state: 'warn' | 'error';
onClose: () => void;
}
class WindowViewModel extends ViewModel<unknown, WindowProps> {
protected onViewMounted() {
this.reaction(() => this.viewProps.state, state => {
console.log(state);
});
}
onCloseClick = () => {
// do something else
this.viewProps.onClose();
};
}
export const Window: FC<WindowProps> = view(WindowViewModel)(({ viewModel, title, state }) => (
<div className={`window window--${state}`}>
<h1>{title}</h1>
<button onClick={viewModel.onCloseClick}>close</button>
</div>
));
Также из-за того, что viewProps
является наблюдаемым, есть дополнительный приятный эффект. Если вложенные компоненты будут каким-то образом зависеть от пропсов своего вью, то при обновлении пропсов вью автоматически они будут сами обновляться.
Меньшая зависимость от хуков жизненного цикла
Про хуки я уже начал говорить в прошлом блоке. Создавать реакции на определенные объекты через useEffect
больше необходимости нет. Но useEffeсt
нужен не только для реакций - его можно также использовать для обработки состояний жизненного цикла компонента. Например, при монтировании, размонтировании и обновлении. И все эти этапы жизненного цикла вполне могут обрабатываться внутри вьюмодели.
Пример
import { observable, makeObservable } from 'mobx';
import { view, ViewModel } from '@yoskutik/react-vvm';
class ComponentViewModel extends ViewModel {
@observable data = undefined;
constructor() {
super();
makeObservable(this);
}
// Например, в моей реализации эта функция замещает вызов
// useLayoutEffect(() => { ... }, []);
protected onViewMountedSync() {
fetch('url')
.then(res => res.json())
.then(res => this.doSomething(res));
}
// А эта частично замещает
// useEffect(() => { ... });
protected onViewUpdated() {
console.log('Some functionality after component updated');
}
doSomething = (res: any) => {};
}
export const Component = view(ComponentViewModel)(({ viewModel }) => (
<div>
{viewModel.data}
</div>
));
Количество пропсов
Это небольшой, но очень приятный бонус. В умных компонентах количество пропсов может свестись к минимуму. В моем прошлом проекте в среднем у каждого умного компонента было не больше 3 пропсов.
Достигается это за счет того, что в MVVM нет необходимости в дриллинге пропсов. Дочерние компоненты вью могут напрямую обращаться к своей вьюмодели. Если они или их вью зависят от состояния вышестоящего вью, они могут обратиться к полю parent
. Если есть зависимость от пропсов вью, то можно обратиться к полю viewProps
. В остальных случаях, конечно, передавать пропсы все ещё нужно напрямую, но даже такая небольшая возможность позволяет в разы сократить количество передаваемых пропсов. Что влияет не только на простоту анализа кода, но и на производительность приложения, т.к. мемоизированному компоненту нужно проверять меньше пропсов при обновлении.
В сравнении с Redux
Чтобы вы ещё лучше осознали всю прелесть MobX с MVVM, я попробую сравнить эту связку с Redux.
Посмотрите на пример ниже. Я расписал тот же код, что в примере по хукам для MVVM, но написанный на Redux.
Пример на Redux
import { FC, useEffect, useLayoutEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IReduxStore } from '../../store';
import { doSomething } from './slice';
export const Component: FC = () => {
const data = useSelector((state: IReduxStore) => state.slice.data);
const dispatch = useDispatch();
useLayoutEffect(() => {
fetch('url')
.then(res => res.json())
.then(res => dispatch(doSomething(res)));
}, []);
useEffect(() => {
console.log('Some functionality after component render');
});
return (
<div>
{data}
</div>
);
}
В этом примере видна сила полноценного деления логики и отображения. В примере с Redux компонент ответственен за обновление состояния. Экшены объявлены где-то в другом месте, но они должны быть использованы внутри компонента. В примере же с MVVM вью не вызывает хуки и никаким образом не обрабатывает данные. Он их только использует.
Что ещё интересно, так это то, что кода в обоих примерах одинаковое количество. На самом деле в MVVM его чуть больше, но кода в конструкторе можно избежать (об этом ниже). Но при этом в Redux нужно ещё создать слайс, а следовательно в Redux кода приходится писать больше.
Вообще количество кода - это отдельный приятный бонус в сравнении MobX и Redux. Я думаю, разработчики, использующие Redux, часто задумываются о том, как же много повторяющегося кода приходится им писать. Redux Toolkit улучшил, конечно, ситуацию, но полноценно избавиться от повторяющегося кода не удалось. Нужно вызывать хук, чтобы получить значение стора; нужно вызывать хук, чтобы получить функцию dispose
; нужно указывать специфичные имена для экшенов и/или слайсов; и т.п.
Ну и типизация. Redux Toolkit и её довольно сильно упростил. Но по мне так она все ещё выглядит костыльно. Получение типа стора по результату функции, возвращающей его; создание редьюсера с определенным типом с помощью дополнительной функции с дженериком; или та же необходимость прокидывать тип стора в каждый вызов useSelector
. Не могу сказать, что это приятные для использования решения.
В примерах же с MobX задумываться о типизации нужно редко. Я создал класс вьюмодели а затем использую его объект. И TypeScript сам расставит всю типизацию.
Вместе с этим, в Redux нужно обязательно думать о мемоизации. Количество useMemo
и useCallback
на квадратный сантиметр кода в Redux может разительно отличаться от этого количества в MobX с MVVM.
Ну и пропсы. На моей практике в Redux довольно часто встречается дриллинг пропсов, хотя бы в рамках вложенности в 1-2 слоя. А, как я написал выше, в MVVM таких проблем меньше.
Но это ещё не все
Согласитесь, MVVM уже выглядит довольно-таки неплохо. Но я решил пойти ещё дальше и немного "прокачать" свою реализацию MVVM.
Автоматический вызов диспозеров
Документация MobX говорит, что вы всегда должны вызывать dispose реакций. Это правило помогает избегать проблем с возникновением утечек памяти. Но следовать этому правилу не очень удобно. Но не в подходе MVVM.
Итак, когда реакции, созданные во вьюмодели, перестают быть нам нужны? Учитывая, что вьюмодель должна умирать после размонтирования компонента, то в большинстве случаев в момент, когда вью размонтируется. Поэтому можно добавить автоматический вызов диспозеров при размонтировании вью. В таком случае реакции можно создавать при помощи небольших функций-надстроек внутри вьюмодели. Интерфейс и типизация при этом у этих функций не будет отличаться от их аналогов в MobX.
Пример создания реакций с автоматической очисткой
import { intercept, makeObservable, observable, observe, when } from 'mobx';
import { ViewModel } from '@yoskutik/react-vvm';
export class SomeViewModel extends ViewModel {
@observable field = 0;
constructor() {
super();
makeObservable(this);
// Для создания реакции можно использовать функцию-алиас
this.reaction(() => this.field, value => this.doSomething(value));
// Для создания авторана можно также использовать функцию-алиас
this.autorun(() => {
this.doSomething(this.field);
});
// А для создания остальных типов обзерваций можно использовать
// функцию addDisposer
// observe
this.addDisposer(
observe(this, 'field', ({ newValue }) => this.doSomething(newValue))
);
// intercept
this.addDisposer(
intercept(this, 'field', change => {
this.doSomething(change.newValue);
return change;
}),
);
// when
const w = when(() => this.field === 1);
w.then(() => this.doSomething(this.field));
this.addDisposer(() => w.cancel());
}
doSomething = (field: number) => {};
}
Удобная конфигурация
Подход MVVM можно гибко подстроить под себя. Например, моя реализация позволяет настроить то, как создается вьюмодель, а также позволяет назначить компонент-обертку для всех вью и чайлдвью. И эти 2 настройки гораздо мощнее, чем вам может показаться. Разумеется, эти настройки можно использовать в том числе и для дебага, но это не основное их назначение.
Например, вы можете сделать вызов функции makeObservable
автоматическим, чтобы не вызывать у каждой вьюмодели эту функцию в отдельности.
Автоматический вызов makeObservable
import { makeObservable, observable } from 'mobx';
import { configure, ViewModel } from '@yoskutik/react-vvm';
configure({
vmFactory: VM => {
const viewModel = new VM();
makeObservable(viewModel);
return viewModel;
},
});
class SomeViewModel extends ViewModel {
// Теперь field1 будет observable, задумываться о вызове makeObservable
// не нужно
@observable field1 = 0;
// Однако, в конструкторе поле ещё не является observable, поэтому
// реакции лучше добавлять в onViewMounted или в onViewMountedSync
protected onViewMounted(): void {
this.reaction(() => this.field1, () => {
// do something
});
}
}
Вы можете создавать вьюмодель таким образом, чтобы в ваш проект можно было добавить паттерн DI (Dependecy Injection). И по мне так это очень приятный бонус. Благодаря DI вы можете создавать маленькие независимые сторы, которые могут использоваться в разных частях вашего проекта. И вместо зависимости от виртуального дерева, с DI вы можете выстраивать зависимости абсолютно свободным образом.
Пример с DI
import { computed, makeObservable, observable } from 'mobx';
// Использовать именно tsyringe не обязательно, подойдет любая DI библиотека
import { injectable, container, singleton } from 'tsyringe';
import { configure, ViewModel } from '@yoskutik/react-vvm';
configure({
vmFactory: VM => container.resolve(VM),
});
// Это пример того самого маленького независимого стора
@singleton()
class SomeOuterClass {
@observable field1 = 0;
constructor() {
makeObservable(this);
}
doSomething = () => {
// do something
};
}
@injectable()
class SomeViewModel extends ViewModel {
@computed get someGetter() {
return this.someOuterClass.field1;
}
// И теперь благодаря DI мы можем спокойно получать нужные нам классы,
// просто указав их в конструкторе
constructor(private someOuterClass: SomeOuterClass) {
super();
makeObservable(this);
}
viewModelDoSomething = () => {
this.someOuterClass.doSomething();
}
}
// Вы так же можете получить объект синглтон класса в любом участке вашего
// приложения
const instance = container.resolve(SomeOuterClass);
Возможность конфигурации обертки для каждого вью мне тоже показалось очень интересной идеей. Мне понравилась возможность в качестве обертки использования Error Boundary. В таком случае все ваши вью будут автоматически оборачиваться в Error Boundary и вам придется меньше думать об обработке ошибок при разработке. Вам может показаться, что это слегка излишне, но в реальности далеко не все компоненты будут вью, а лишь те, что содержат много логики.
Пример с Error Boundary
import { Component, ReactElement, ErrorInfo } from 'react';
import { configure } from '@yoskutik/react-vvm';
class ErrorBoundary extends Component<{ children: ReactElement }, { hasError: boolean }> {
static getDerivedStateFromError() {
return { hasError: true };
}
state = {
hasError: false,
};
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(error, info);
}
render() {
return !this.state.hasError && this.props.children;
};
}
configure({
Wrapper: ErrorBoundary,
});
Так значит MVVM - это ультимативное решение всех проблем?
Поздравляю, вы почти дошли до конца статьи. Но, нет, к сожалению MVVM - это не ультимативное решение всех проблем.
MVVM позволяет решить многое. По мне так очень многое. Мне правда нравится этот паттерн. Но есть одна серьезная проблема. Бывают такие моменты, когда некоторые данные нужны в абсолютно разрозненных частях проекта.
Снова граф
Снова граф React-компонет. Желтым цветом я пометил компоненты, которые зависят от одного набора данных.
Если такие компоненты находятся относительно близко друг к другу, то вполне реально разместить данные где-нибудь в родительской вьюмодели. Однако, с увеличением глубины, количество используемых родителей может возрастать. Согласитесь, если придется получать данные через viewModel.parent.parent.parent.parent.data
, то это уже совсем неудобно. Да и если хранить во вьюмодели данные, которые нужны парочке детей на огромной глубине внутри, то вьюмодель будет невероятно сильно разрастаться.
В данной проблеме мы снова возвращаемся к тому, что нужно использовать некоторые независимые сторы, которые никак не привязаны к виртуальному дереву, а следовательно такие сторы никак не облагаются строгостью MVVM.
Но решение есть. И я даже думаю, что вы наверняка уже догадались о чем я говорю. Паттерн DI может наложить ту самую строгость. И он довольно-таки хорошо ложится на использование совместно с MVVM. И вот MVVM + DI уже выглядит как полноценное решение для крупных проектов, которое позволит долго масштабировать проект.
В этой статье я и так уже много написал, так что здесь в особенности DI я особо углубляться не буду. Однако, вы можете почитать о комбинации паттернов MVVM и DI в моей следующей статье.
Конец
Спасибо за уделенное время. Не стесняйтесь делиться своим мнением в комментариях, с удовольствием почитаю их.
А ещё, как я говорил, я подготовил крошечную библиотеку, буквально 300 строчек кода. Вы можете её использовать, если хотите поиграться с паттерном MVVM. Вот ссылки: npm, github, документация. А ещё можете посмотреть на пару примеров её использования.