Sitemap
Shraddha Dongol

Shraddha Dongol

Architecture Overview: MVVM + RTK Query in React

2 min readSep 27, 2026

--

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.

Get Shraddha Dongol’s stories in your inbox

Join Medium for free to get updates from this writer.

export interface User {
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.

--

--