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 logic

When 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

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.