Mastering MVVM Architecture in React Native (Expo SDK 54)
As mobile apps scale, state management and separation of concerns become essential.
Adopting the MVVM (Model–View–ViewModel) pattern in React Native helps you maintain clean, testable, and maintainable code — especially when working with frameworks like Expo SDK 54.
What is MVVM?
MVVM stands for:
- Model → Business logic & data sources (API, DB, storage).
- View → UI components built using React Native (screens, widgets).
- ViewModel → Connects Model and View, handles state & exposes data for rendering.
In React Native, you can implement the ViewModel layer using:
- Zustand, Recoil, or Jotai for global state.
- React Context + Hooks for local state management.
- Or even Redux Toolkit for larger apps.
MVVM Data Flow in React Native
Here’s a simple conceptual diagram showing the data flow in MVVM for React Native:
┌───────────────────────────────┐
│ User Input │
│ (Press, Type, Gesture) │
└───────────────┬───────────────┘
│
▼
┌──────────────┐
│ ViewModel │ ←── Handles Logic + State
│ (Hooks/Store)│
└──────┬───────┘
│
┌──────────────┴──────────────┐
│ Model │
│ (API, DB, Async Storage) │
└──────────────┬──────────────┘
│
▼
┌──────────────┐
│ View │ ←── React Components
│ (UI Render) │
└──────────────┘
The View observes data changes from the ViewModel, and the ViewModel fetches or updates data via the Model.
Whenever the state in the ViewModel changes, React automatically re-renders the View.
Folder Structure (Recommended)
A scalable project layout for MVVM with Expo:
src/
├── models/
│ └── user.model.ts
├── services/
│ └── user.service.ts
├── viewmodels/
│ └── user.viewmodel.ts
├── views/
│ └── UserScreen.tsx
├── hooks/
│ └── useViewModel.ts
├── App.tsx
Example: Managing User Profile (MVVM in React Native)
Let’s go through a simple example that demonstrates fetching and updating user data.
Model – user.model.ts
// src/models/user.model.ts
export interface User {
id: string;
name: string;
email: string;
}
Service Layer – user.service.ts
This simulates your data source (could be an API, local DB, or async storage).
// src/services/user.service.ts
import { User } from "../models/user.model";
export const fetchUser = async (): Promise<User> => {
// Mock API call
await new Promise((res) => setTimeout(res, 500));
return { id: "1", name: "Praise CE", email: "praise@example.com" };
};
export const saveUser = async (user: User): Promise<void> => {
// Save to backend or local storage
await new Promise((res) => setTimeout(res, 500));
console.log("✅ User saved:", user);
};
ViewModel – user.viewmodel.ts
We’ll use Zustand to store and manage state reactively.
// src/viewmodels/user.viewmodel.ts
import { create } from "zustand";
import { fetchUser, saveUser } from "../services/user.service";
import { User } from "../models/user.model";
interface UserState {
user: User | null;
loading: boolean;
fetchUser: () => Promise<void>;
updateUser: (updates: Partial<User>) => Promise<void>;
}
export const useUserViewModel = create<UserState>((set, get) => ({
user: null,
loading: false,
fetchUser: async () => {
set({ loading: true });
const user = await fetchUser();
set({ user, loading: false });
},
updateUser: async (updates) => {
const current = get().user;
if (!current) return;
const updated = { ...current, ...updates };
set({ user: updated });
await saveUser(updated);
},
}));
View – UserScreen.tsx
// src/views/UserScreen.tsx
import React, { useEffect, useState } from "react";
import { View, Text, TextInput, Button, ActivityIndicator } from "react-native";
import { useUserViewModel } from "../viewmodels/user.viewmodel";
export const UserScreen = () => {
const { user, loading, fetchUser, updateUser } = useUserViewModel();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
useEffect(() => {
fetchUser();
}, []);
useEffect(() => {
if (user) {
setName(user.name);
setEmail(user.email);
}
}, [user]);
if (loading) return <ActivityIndicator size="large" />;
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 22, fontWeight: "600", marginBottom: 10 }}>Profile</Text>
<TextInput
placeholder="Name"
value={name}
onChangeText={setName}
style={{ borderWidth: 1, marginBottom: 10, padding: 8 }}
/>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={{ borderWidth: 1, marginBottom: 10, padding: 8 }}
/>
<Button
title="Save"
onPress={() => updateUser({ name, email })}
disabled={loading}
/>
</View>
);
};
MVVM + AsyncStorage (Data Persistence Example)
To persist user data between sessions, integrate AsyncStorage:
// src/services/user.service.ts (extended)
import AsyncStorage from "@react-native-async-storage/async-storage";
import { User } from "../models/user.model";
const USER_KEY = "app_user";
export const fetchUser = async (): Promise<User> => {
const saved = await AsyncStorage.getItem(USER_KEY);
if (saved) return JSON.parse(saved);
return { id: "1", name: "Guest", email: "guest@example.com" };
};
export const saveUser = async (user: User) => {
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
};
Why MVVM Works So Well in React Native
- Decoupled logic: UI and business logic live in separate layers.
- Reusability: The ViewModel can power multiple screens.
- Testing: You can test logic without rendering UI.
- Scalability: Ideal for large, data-driven apps.
Key Takeaways
- MVVM helps organize React Native apps for growth and maintainability.
- Zustand, Recoil, or Context Hooks can be used as your ViewModel state layer.
- AsyncStorage, SQLite, or APIs act as your data Model layer.
- The View simply observes and reacts to ViewModel changes.
💡 Expo SDK 54 provides excellent TypeScript, HMR, and local storage support — making MVVM easier and faster to implement than ever.

