React Native MVVM Generator
Scaffold scalable React Native features using the MVVM (Model–View–ViewModel) pattern with a single command.
Stop copy-pasting the same four files every time you add a screen. Run one command, type a name, and get a feature folder wired up exactly the way the rest of your codebase expects it.
What it generates
Running RN MVVM: Create Feature with the name Profile produces:
src/features/Profile/
├── ProfileView.tsx # React Native functional component
├── useProfileViewModel.ts # State + actions hook
├── profileService.ts # API layer (fetch/update placeholders)
├── profileTypes.ts # Feature-local TypeScript types
└── index.ts # Barrel export (optional)
Each file follows consistent naming:
| File |
Convention |
View |
PascalCase (ProfileView.tsx) |
ViewModel |
camelCase hook (useProfileViewModel.ts) |
Service |
camelCase (profileService.ts) |
Types |
camelCase (profileTypes.ts) |
The View imports the ViewModel, the ViewModel imports the Service, and the Service imports the Types. Zero circular deps.
Usage
Option 1 — Command Palette
Ctrl/Cmd + Shift + P
- Run RN MVVM: Create Feature
- Enter a feature name (e.g.
Profile, UserSettings, Checkout)
- Confirm the base folder (defaults to
src/features)
- Pick TypeScript or JavaScript
Option 2 — Right-click a folder
Right-click any folder in the Explorer → RN MVVM: Create Feature in This Folder. The feature is created directly inside that folder, skipping the base-folder prompt.
Settings
All settings live under rnMvvm.* in your settings.json (or the Settings UI, searching "React Native MVVM"):
| Setting |
Default |
Description |
rnMvvm.language |
"typescript" |
Default language for generated files (typescript or javascript). |
rnMvvm.defaultBaseFolder |
"src/features" |
Path relative to the workspace root where features are created. |
rnMvvm.useIndexFile |
true |
Generate an index.ts barrel that re-exports the View/ViewModel/Service. |
rnMvvm.styleStrategy |
"stylesheet" |
How the generated View styles itself: stylesheet, inline, or none. |
rnMvvm.overwritePolicy |
"prompt" |
What to do if files already exist: prompt, skip, or overwrite. |
Why MVVM for React Native?
A few things fall into place once each feature is split into View / ViewModel / Service / Types:
- Testable logic. The ViewModel is a plain hook — you can unit-test it with
@testing-library/react-hooks or @testing-library/react-native without rendering any UI.
- Swappable transport. The Service is the only module that talks to the network. Swap
fetch for axios, add a retry policy, or mock it in tests without touching the View.
- Predictable file layout. New team members know exactly where to look. No more "is this state in the screen or in Redux or in a context provider?"
- Feature-local types. Types live next to the code that uses them, not in a 2,000-line
types.ts at the repo root.
Example output
ProfileView.tsx
import React from 'react';
import { View, Text, Button, ActivityIndicator, StyleSheet } from 'react-native';
import { useProfileViewModel } from './useProfileViewModel';
export interface ProfileViewProps {
// Add any props your screen needs to receive from navigation or parents here.
}
const ProfileView: React.FC<ProfileViewProps> = () => {
const { state, actions } = useProfileViewModel();
if (state.isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator />
</View>
);
}
if (state.error) {
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text>{state.error}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Profile</Text>
{state.data ? (
<Text>{JSON.stringify(state.data, null, 2)}</Text>
) : (
<Text>No data yet.</Text>
)}
<Button title="Reload" onPress={actions.refresh} />
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 12 },
title: { fontSize: 20, fontWeight: '600' },
});
export default ProfileView;
useProfileViewModel.ts
import { useCallback, useEffect, useState } from 'react';
import { profileService } from './profileService';
import type { ProfileData } from './profileTypes';
interface ProfileState {
isLoading: boolean;
error: string | null;
data: ProfileData | null;
}
export function useProfileViewModel() {
const [state, setState] = useState<ProfileState>({
isLoading: true,
error: null,
data: null,
});
const load = useCallback(async () => {
setState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const data = await profileService.fetchProfile();
setState({ isLoading: false, error: null, data });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error';
setState({ isLoading: false, error: message, data: null });
}
}, []);
useEffect(() => {
void load();
}, [load]);
return { state, actions: { refresh: load } };
}
Commands
| Command |
ID |
| RN MVVM: Create Feature |
rnMvvm.createFeature |
| RN MVVM: Create Feature in This Folder |
rnMvvm.createFeatureHere |
Troubleshooting
Nothing happens when I run the command.
Open the React Native MVVM output channel (View → Output, then pick it from the dropdown) — every action is logged there.
I got an error about the folder already existing.
That's the overwrite policy doing its job. Either delete the existing folder, or change rnMvvm.overwritePolicy to overwrite or skip.
The generated types don't match my API.
They're starter boilerplate — edit featureNameTypes.ts to reflect your real domain. The generator doesn't introspect your API (yet — see Roadmap).
Contributing
PRs welcome. The code is intentionally modular so new templates are easy to add — drop a file in src/templates/, wire it into featureGenerator.ts, and you're done.
License
MIT