First touch
Hi team, hope you are doing well!
This is my first time here, and in this article, I want to walk you through architecting a front-end application using React/Vite with the MVVM architecture. It will not be a traditional MVVM architecture, as I have updated and optimized it to suit the React structure.
I spent 3 days on this architecture, and in this article, we will just answer the WHY questions. I will not go deep into the HOW (to install) questions because you can easily find that information on the internet.
I think this will be helpful for beginners.
Letโs get started!
What is MVVM?
MVVM is a design pattern that separates concerns by dividing the application into three components: Models (data handling), Views (UI), and ViewModels (interaction logic). This separation enhances code maintainability and scalability, making it easier to manage large codebases.
What is vite?
Vite is a modern build tool that provides a lightning-fast development experience. With Vite, you get instant server startup, optimized builds, and seamless integration with TypeScript and React. Itโs the perfect companion for developing modern web applications. Vite docs
Key Features
Project structure
Explanation of the folder structure and organization.
๐ assets
Purpose: Store all images and static assets.
Guideline: Place image files and other static assets in this directory.
๐ง config
Purpose: Store configuration files.
Guideline: Include environment settings, theme configurations, authentication settings, etc., in this directory.
๐ core
client: Contains Axios configuration.
models: Defines models and their mappings.
repositories: Defines abstract classes for repositories, acting as contracts between presentation and infrastructure layers.
repositoryImpl: Implements the repository classes.
useCases: Defines the moduleโs behavior (e.g., UserUseCase -> getAll, getById).
โ๏ธ enum
Purpose: Store global constants.
Guideline: Place enumerations and global constants in this directory.
๐ presentations
components: Contains shared components and their stories.
hooks: Contains shared custom hooks.
layout: Defines the layout of the application (e.g., useAuth, useLocalStorage).
pages: Contains application pages.
routers: Defines application routes, utilizing dynamic import to split chunks at build time.
viewModels: Defines the viewModel layer as a hook to manage business logic and provide data for the view layer.
๐ providers
Purpose: Store global providers.
Guideline: Include all global providers in this directory.
๐๏ธ schemas
Purpose: Store schema validations.
Guideline: Include schema validations for all input fields in this directory.
๐ types
Purpose: Store types and interfaces.
Guideline: Include types and interfaces such as APIRequest, APIResponse, etc., in this directory.
๐จ utils
Purpose: Store utility functions.
Guideline: Place all utility functions in this directory. Include the utility functions in the utils folder (e.g: helpers, conversion factors)
MVVM Implementation
In this section, I will discuss the implementation of the MVVM architecture, from the View of the API calling service. Letโs pick the Login feature for an example (the example repository will be placed at the end of this article or here)
To implement the Login feature you have to build the View layer ๐
export default function LoginPage() {
const { onLogin } = useAuthViewModel();
return (
<Container maxWidth="xs">
<CssBaseline />
<Box
sx={{
pt: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Login
</Typography>
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={loginValidationSchema}
onSubmit={onLogin}
>
{({ isSubmitting }) => (
<Form style={{ width: "100%", marginTop: 1 }}>
<Field
component={CustomTextField}
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
required
/>
<Field
component={CustomTextField}
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
required
/>
<FormControlLabel
control={
<Field
as={Checkbox}
name="remember"
color="primary"
/>
}
label="Remember me"
/>
<CustomButton
label="Login"
loading={isSubmitting}
/>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link
// href={AuthRoute.register}
variant="body2"
>
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Form>
)}
</Formik>
</Box>
</Container>
);
}
Look at the useAuthViewModel
hooks, and why we need the ViewModel
layer.
In this case, the LoginPage (View
layer) only handles things related to the user interface. It is used to render data from the ViewModel
.
Letโs move to the ViewModel
layer.
export const useAuthViewModel = () => {
const { execute: _login, loading: isLogging } = useFetching<AuthModel>(
authUseCase.login
);
const { execute: _logout } = useFetching<void>(authUseCase.logout);
const { showSnackbar } = useSnackbar();
const { setValue } = useLocalStorage(AUTH_KEY.token);
const { getCurrentUser } = useAuth();
const navigator = useNavigate();
const onLogin = async (payload: LoginPayload) => {
const { data, error } = await _login<LoginPayload>(payload);
if (data) {
setValue({ ...data });
showSnackbar("Login successfully", "success");
getCurrentUser();
window.location.replace(import.meta.env.VITE_APP_URL);
}
if (error) {
showSnackbar(error.message, "error");
}
};
const logout = async () => {
setValue(null);
await _logout<LoginPayload>();
navigator(AUTH_ROUTES.login);
};
return {
isLogging,
onLogin,
logout,
};
};
In the ViewModel
layer, we have an onLogin function. This function is invoked when the user clicks the login button. It encapsulates the entire login logic, managing both success and error scenarios. When the login is successful, it stores the user's token, displays a success message, and redirects the user to the main application. In the event of an error, it shows an appropriate error message to the user. This separation of concerns ensures that the View
layer focuses solely on the user interface, while the ViewModel
layer handles the business logic and state management.
And take a look at the authUseCase.login
, and we explored more of the use-case class.
In this class, we declare the methods of the module such as Login, Register, and the mapping factory (if you need to transform the results from the APIs), see the example below:
export class AuthUseCase {
constructor(private readonly authRepository: AuthRepository) {
classUtils.autoBind(this);
}
async login(payload: LoginPayload): Promise<AuthModel> {
const res = await this.authRepository.login(payload);
return AuthModel.mapper(res);
}
}
/* */
export class AuthModel {
accessToken: string;
refreshToken: string;
static mapper(data: unknown): AuthModel {
const authModel = new AuthModel();
authModel.accessToken = data['at'] as string;
authModel.refreshToken = data['rt'] as string;
return authModel;
}
}
The AuthUseCase
handles the application's core business logic, defined and implemented through use case classes, each corresponding to specific business actions. It encapsulates the system's business logic for other system layers.
This outermost layer provides infrastructure and services for other layers, including code for interacting with databases, web services, and external systems. For example, a AuthRepository
class to send and receive requests to the server.
export abstract class AuthRepository {
abstract login(payload: LoginPayload): Promise<LoginResponse>;
}
/* */
export class AuthRepositoryImpl implements AuthRepository {
_client: APIClientImpl;
private readonly _prefix = "auth";
constructor() {
this._client = new APIClientImpl(this._prefix);
}
async login(payload: LoginPayload): Promise<LoginResponse> {
return await this._client.post<LoginResponse, LoginPayload>(
"/login",
payload
);
}
}
The AuthRepository
serves as a contract layer between the use-case and the API client. For API client configuration, refer to this link.
Following the example above, we separate the Model layer into smaller modules: Model - UseCase - Repository. The purpose of this is that each layer is responsible for a specific group of tasks within a module. We make it scalable and maintainable, even as it gets larger.
Why Use Storybook?
Storybook offers a multitude of benefits for React developers:
- Component Isolation: Develop and test components in isolation, without the complexities of the entire application. This fosters better component reusability and maintainability.
- Rapid Development: Iterate quickly on component designs and interactions without affecting the main application.
- Living Styleguide: Create a visual styleguide for your design system, making it easy to share and maintain design consistency across the project.
- Improved Collaboration: Share components and design decisions with other team members through Storybook.
- Automated Testing: Integrate testing frameworks to ensure component quality and prevent regressions.
- Enhanced Developer Experience: Streamline development workflows by providing a dedicated environment for component development.
import CustomButton from "./CustomButton";
export default {
title: "Components/CustomButton",
component: CustomButton,
argTypes: {
label: { control: "text" },
loading: { control: "boolean" },
onClick: { action: "clicked" },
},
};
const Template = (args) => <CustomButton {...args} />;
export const Primary = Template.bind({});
Primary.args = {
label: "Primary Button",
loading: false,
};
export const Loading = Template.bind({});
Loading.args = {
label: "Loading Button",
loading: true,
};
export const Disabled = Template.bind({});
Disabled.args = {
label: "Disabled Button",
disabled: true,
};
/* */
const CustomButton: React.FC<CustomButtonProps> = ({
label,
loading = false,
...props
}) => {
return (
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
disabled={loading}
{...props}
disableElevation
onLoad={() => loading}
>
{label}
</Button>
);
};
A story shows how a UI component should look on your browser. With Storybook, you can write multiple stories based on every change you want to make to your component. This means that rather than changing elements in the DOM, Storybook updates the information a user sees based on the stories youโve created.
Example code
You can find the example source code in this Repository, and donโt forget to start the repository if you like it. Thanks!
Final works
In this article, I've gone over how to apply MVVM to the ReactJs project, It has many mistakes because it's my first post, so if you are a beginner hope you will get some information that is helpful for your career and if you are an expert hope you leave a comment for me to improve and make it better in the future. Thank you all.
Top comments (1)