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:
Model
Data structures, business logic, and data sources (API, storage).
View
UI components that display data and capture user interactions.
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
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.tsExample: User Entity
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
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(),
});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
src/
├── app/ # Expo Router pages
│ └── (tabs)/
│ └── profile.tsx # Route component
│
└── presentation/
└── screens/
└── ProfileScreen.tsx # Screen componentExample: Profile Screen
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
• 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
src/
└── presentation/
└── viewmodels/
└── useProfileViewModel.tsExample: Profile ViewModel
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,
};
}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.