MVVM: Model View ViewModel
MVVM (Model-View-ViewModel) — архитектурный паттерн, в котором ViewModel содержит логику представления и состояние UI, а View автоматически обновляется при изменении ViewModel через механизм привязки данных (data binding).
Зачем нужно
MVVM решает главную проблему MVC и MVP — ручную синхронизацию View и данных. В MVVM это автоматизировано: ViewModel хранит observable-состояние, View подписана и обновляется реактивно. React с его useState и useReducer следует духу MVVM: компонент автоматически перерендеривается при изменении state. Понимание MVVM помогает проектировать архитектуру компонентов с разделением UI-логики и бизнес-логики.
Где используется
- WPF, Xamarin, SwiftUI, Jetpack Compose — нативные платформы
- Vue.js — реактивность через data binding (очень близко к MVVM)
- Angular — двусторонний binding через
[(ngModel)] - React с MobX — observable store + автоматический ре-рендер
- React + Zustand/Jotai — однонаправленный вариант MVVM
Структура MVVM
┌──────────┐ Data Binding ┌────────────┐ ┌───────┐
│ View │◄──────────────────►│ ViewModel │───►│ Model │
│ │ (реактивно) │ │ │ │
└──────────┘ └────────────┘ └───────┘
View — компоненты, шаблоны, HTML
ViewModel — состояние UI, команды, форматирование
Model — данные, бизнес-логика, API
MVVM в React: компонент как View + ViewModel
// ViewModel как кастомный хук
// Инкапсулирует UI-логику, тестируется без рендера
interface Product {
id: string;
name: string;
price: number;
discount?: number;
}
// ViewModel
function useProductViewModel(productId: string) {
// Состояние UI (ViewModel state)
const [quantity, setQuantity] = useState(1);
const [isAddingToCart, setIsAddingToCart] = useState(false);
// Данные из Model (API/store)
const { data: product, isLoading, error } = useProduct(productId);
// Вычисляемые значения (форматирование для View)
const formattedPrice = product
? new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' })
.format(product.price * (1 - (product.discount ?? 0)))
: '';
const hasDiscount = Boolean(product?.discount);
const canAddMore = quantity < 99;
const canAddLess = quantity > 1;
// Команды (ViewModel actions)
const incrementQuantity = () => setQuantity(prev => Math.min(prev + 1, 99));
const decrementQuantity = () => setQuantity(prev => Math.max(prev - 1, 1));
const addToCart = async () => {
if (!product) return;
setIsAddingToCart(true);
try {
await cartApi.addItem({ productId, quantity });
} finally {
setIsAddingToCart(false);
}
};
return {
// Данные для View
product,
isLoading,
error,
formattedPrice,
hasDiscount,
quantity,
canAddMore,
canAddLess,
isAddingToCart,
// Команды для View
incrementQuantity,
decrementQuantity,
addToCart,
};
}
// View — только рендеринг, никакой логики
function ProductPage({ productId }: { productId: string }) {
// View использует ViewModel через хук
const vm = useProductViewModel(productId);
if (vm.isLoading) return <Skeleton />;
if (vm.error) return <ErrorMessage error={vm.error} />;
if (!vm.product) return null;
return (
<div>
<h1>{vm.product.name}</h1>
<p className={vm.hasDiscount ? 'price--discounted' : 'price'}>
{vm.formattedPrice}
</p>
<div className="quantity">
<button onClick={vm.decrementQuantity} disabled={!vm.canAddLess}>-</button>
<span>{vm.quantity}</span>
<button onClick={vm.incrementQuantity} disabled={!vm.canAddMore}>+</button>
</div>
<button onClick={vm.addToCart} disabled={vm.isAddingToCart}>
{vm.isAddingToCart ? 'Добавляем...' : 'В корзину'}
</button>
</div>
);
}
Частые ошибки
- Логика в View — если компонент содержит форматирование, вычисления, условную логику — это нарушение MVVM; переносите в хук/ViewModel.
- ViewModel знает о DOM — ViewModel должна работать с чистыми данными; DOM-операции (
document.querySelector) в ней недопустимы. - Жирный ViewModel — ViewModel не должен содержать бизнес-правила (скидки, валидацию бизнеса); это задача Model/Service.
Связанные темы
- _MOC SPA
- MVC на Node.js
- MVP -- Model View Presenter
- State -- внутреннее состояние
- Clean Architecture на фронтенде