MVVM Architecture

PHL RN Boilerplate implements the Model-View-ViewModel (MVVM) pattern, providing clear separation of concerns and making your codebase more maintainable and testable.

What is MVVM?

MVVM is an architectural pattern that separates your application into three distinct layers:

M

Model

Data structures, business logic, and data sources (API, storage).

V

View

UI components that display data and capture user interactions.

VM

ViewModel

Bridge between Model and View, managing presentation logic and state.

Architecture Overview

Here's how data flows through the MVVM pattern:

┌─────────────────────────────────────────────────────────┐
│                          View                           │
│  (Screens, Components - UI Only)                        │
│                                                         │
│  • Displays data from ViewModel                         │
│  • Captures user input                                  │
│  • No business logic                                    │
└────────────────────┬────────────────────────────────────┘
                     │ observes state
                     │ calls actions
                     ▼
┌─────────────────────────────────────────────────────────┐
│                       ViewModel                         │
│  (Hooks - Presentation Logic)                           │
│                                                         │
│  • Manages UI state                                     │
│  • Formats data for display                             │
│  • Exposes actions to View                              │
│  • Orchestrates Model operations                        │
└────────────────────┬────────────────────────────────────┘
                     │ reads/writes
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│                         Model                           │
│  (Entities, API, Storage - Data & Business Logic)       │
│                                                         │
│  • Domain entities and types                            │
│  • API clients and data fetching                        │
│  • Local storage (MMKV)                                 │
│  • Business rules and validation                        │
└─────────────────────────────────────────────────────────┘

Model Layer

The Model represents your application's data and business logic. It's independent of the UI and can be reused across different platforms.

Model Folder Structure

typescript
src/
├── domain/               # Business layer
│   ├── entities/         # Type definitions
│   │   └── user.ts
│   └── schemas/          # Validation schemas
│       └── user.schema.ts
│
└── data/                 # Data layer
    ├── api/              # HTTP clients
    │   ├── client.ts
    │   └── endpoints/
    │       └── users.ts
    └── storage/          # Local persistence
        └── user.storage.ts

Example: User Entity

src/domain/entities/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

export interface UserProfile extends User {
  bio: string;
  website?: string;
  socialLinks: {
    twitter?: string;
    github?: string;
  };
}

Example: Validation Schema

src/domain/schemas/user.schema.ts
import { z } from 'zod';

export const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  avatar: z.string().url().optional(),
});

export const updateProfileSchema = z.object({
  name: z.string().min(2).max(50),
  bio: z.string().max(200),
  website: z.string().url().optional(),
});
Learn more about the Model layer in the Model Layer Guide.

View Layer

The View is responsible only for rendering UI. It receives data from the ViewModel and delegates all actions back to it. Views should be "dumb" components with no business logic.

View Folder Structure

typescript
src/
├── app/                        # Expo Router pages
│   └── (tabs)/
│       └── profile.tsx         # Route component
│
└── presentation/
    └── screens/
        └── ProfileScreen.tsx   # Screen component

Example: Profile Screen

src/presentation/screens/ProfileScreen.tsx
import { View, Text, Image, TouchableOpacity } from 'react-native';
import { useProfileViewModel } from '../viewmodels/useProfileViewModel';

export function ProfileScreen() {
  // Get all data and actions from ViewModel
  const vm = useProfileViewModel();

  if (vm.isLoading) {
    return <LoadingSpinner />;
  }

  if (vm.error) {
    return <ErrorMessage message={vm.error} />;
  }

  return (
    <View className="flex-1 bg-slate-950 p-4">
      <Image
        source={{ uri: vm.user.avatar }}
        className="w-24 h-24 rounded-full"
      />
      <Text className="text-2xl font-bold text-white mt-4">
        {vm.user.name}
      </Text>
      <Text className="text-slate-400">
        {vm.user.email}
      </Text>
      
      <TouchableOpacity
        onPress={vm.handleEditProfile}
        className="mt-6 bg-blue-500 px-6 py-3 rounded-lg"
      >
        <Text className="text-white font-semibold">
          Edit Profile
        </Text>
      </TouchableOpacity>
    </View>
  );
}
View Best Practices
• No business logic or data manipulation
• Only render UI based on ViewModel state
• Call ViewModel actions for all interactions
• Keep components focused and reusable

ViewModel Layer

The ViewModel is the brain of your feature. It manages state, processes data, and exposes everything the View needs through a clean interface.

ViewModel Folder Structure

typescript
src/
└── presentation/
    └── viewmodels/
        └── useProfileViewModel.ts

Example: Profile ViewModel

src/presentation/viewmodels/useProfileViewModel.ts
import { useCallback } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { router } from 'expo-router';
import { userApi } from '@/data/api/endpoints/users';
import { useAuthStore } from '@/stores/auth.store';

export function useProfileViewModel() {
  const userId = useAuthStore(state => state.userId);
  
  // Fetch user data
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => userApi.getUser(userId),
  });

  // Update profile mutation
  const updateMutation = useMutation({
    mutationFn: userApi.updateProfile,
    onSuccess: () => {
      // Refetch user data
      queryClient.invalidateQueries(['user', userId]);
    },
  });

  // Actions
  const handleEditProfile = useCallback(() => {
    router.push('/edit-profile');
  }, []);

  const handleUpdateProfile = useCallback((data: UpdateProfileInput) => {
    updateMutation.mutate(data);
  }, [updateMutation]);

  // Return interface for the View
  return {
    // State
    user,
    isLoading,
    error: error?.message,
    isUpdating: updateMutation.isPending,
    
    // Actions
    handleEditProfile,
    handleUpdateProfile,
  };
}
ViewModels are custom React hooks that encapsulate all the logic for a screen or feature.

Benefits of MVVM

✅ Separation of Concerns

Each layer has a single responsibility, making code easier to understand and maintain.

✅ Testability

ViewModels can be tested independently without rendering UI, making unit tests faster and more reliable.

✅ Reusability

Logic in ViewModels can be shared across multiple Views or even different platforms.

✅ Collaboration

Teams can work in parallel: designers on Views, developers on ViewModels and Models.

Next Steps