Architecture Overview: MVVM + RTK Query in React
This blog outlines how the Model-View-ViewModel (MVVM) pattern is structured and integrated with RTK Query in this project’s src directory.
Folder Structure Reference
models/: TypeScript interfaces and types representing business data structures (e.g.,user.model.ts).slice/: RTK Query API slices and Redux Toolkit slices handling server-state caching, fetching, and mutations.view-models/: Custom hooks (use*.ts) that encapsulate business logic, UI state manipulation, data filtering, and connect to RTK Query hooks.views/: Pure UI React components responsible only for layout, rendering, and passing user interactions to the ViewModel.routes/: Application routing configuration.utils/: Shared helper functions and utility modules.
Data Flow Architecture
[ View (UI Component) ]
│
▼ (Invokes & Observes)
[ ViewModel (Custom Hook) ]
│
├─────────────────────────┐
▼ ▼
[ RTK Query (`slice/`) ] [ Local UI State (`useState`) ]
│
▼
[ Backend API / Mock ]1. Model (src/models/user.model.ts)
Defines the shape of the data entity.
export interface User {Get Shraddha Dongol’s stories in your inbox
Join Medium for free to get updates from this writer.
id: string;
name: string;
email: string;
}
2. RTK Query Slice (src/slice/userApi.ts)
Handles API communication and caching.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { User } from '../models/user.model';
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
}),
}),
});
export const { useGetUsersQuery } = userApi;3. ViewModel (src/view-models/useUserViewModel.ts)
Encapsulates data fetching and local business/UI logic.
import { useState } from 'react';
import { useGetUsersQuery } from '../slice/userApi';
export const useUserViewModel = () => {
const { data: users, isLoading, error } = useGetUsersQuery();
const [searchQuery, setSearchQuery] = useState('');
const filteredUsers = users?.filter((user) =>
user.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return {
users: filteredUsers,
isLoading,
error,
searchQuery,
setSearchQuery,
};
};4. View (src/views/UserView.tsx)
Pure rendering component free of heavy business or data-fetching logic.
import React from 'react';
import { useUserViewModel } from '../view-models/useUserViewModel';
export const UserView: React.FC = () => {
const { users, isLoading, error, searchQuery, setSearchQuery } = useUserViewModel();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load users.</div>;
return (
<div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Filter users..."
/>
<ul>
{users?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};Benefits of this Architecture
- Strict Separation of Concerns: Components stay clean and declarative. Network state handling lives in RTK Query, while UI behavior lives in ViewModels.
- Enhanced Testability: ViewModels can be tested independently of React component trees using standard hook testing utilities.
- Scalability: Easily manage complex client-side transformations and side-effects without cluttering UI components or global Redux stores.








