MVVM Pattern
Technical description of the MVVM (Model-View-ViewModel) pattern used in Ledger Wallet to separate UI components from business logic.
Pattern Structure
ComponentName/
├─ index.tsx # Exports the connected component
└─ useComponentNameViewModel.ts # Contains business logicWhen a component connects to outer layers (network calls, store data), create a dedicated hook named use{ComponentName}ViewModel. The ViewModel suffix identifies hooks dedicated to a specific component that are not intended for reuse elsewhere.
Implementation
Correct Way
Inject the ViewModel hook result as props:
// index.tsx
import { useMarketListViewModel } from "./useMarketListViewModel";
function View(props) {
// Pure UI component - only renders based on props
return (
<div>
<h1>{props.title}</h1>
<ul>
{props.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={props.onRefresh}>Refresh</button>
</div>
);
}
// Connect View to ViewModel
const MarketList = () => <View {...useMarketListViewModel()} />;
export default MarketList;// useMarketListViewModel.ts
import { useSelector, useDispatch } from "react-redux";
import { useCallback } from "react";
import { selectMarketItems, fetchMarketData } from "../data-layer";
export function useMarketListViewModel() {
const dispatch = useDispatch();
const items = useSelector(selectMarketItems);
const isLoading = useSelector(selectIsLoading);
const onRefresh = useCallback(() => {
dispatch(fetchMarketData());
}, [dispatch]);
return {
title: "Market",
items,
isLoading,
onRefresh,
};
}Wrong Way
🚫
Don’t mix ViewModel logic directly in the component:
// ❌ Wrong - mixing concerns
function MarketList() {
const props = useMarketListViewModel();
// Business logic mixed with UI
if (props.isLoading) {
return <Spinner />;
}
return <div>{props.data}</div>;
}
export default MarketList;⚠️
Why? Separating the ViewModel call from the View makes testing easier. You can test the View with different props without mocking hooks, and test the ViewModel logic independently.
When to Use
- Components that connect to Redux store
- Components that make API calls
- Components with complex business logic
- Screens and major feature components
When NOT to Use
- Simple presentational components
- Components that only receive props from parents
- Pure utility components (buttons, inputs, etc.)
See also
- Component Patterns — Rule 9 covers the
useViewModelinjection pattern - How to Structure a Feature — How to set up a feature module with MVVM
- Architecture Decisions — Why we use MVVM and its benefits