coiai Logo

This article is an English translation of the original Japanese post. Read the Japanese original →

What Is MVVM? Building a Simple App with React to Understand It

July 15, 2024

Categories: React, おすすめ

❓ What Is This

MVVM is one of the design models used in frontend development.
To get a feel for it, we'll build a simple task-tracker app with React and TypeScript.

This should be approachable even if you're brand new to React with TypeScript.
It should take about 15 minutes.

Here's what we'll end up with.

🪴 Environment

Written on Tuesday, July 16, 2024

  • Apple M1 MacBook Air
  • 14.5 (23F79)

🛠️ How To

MVVM is a software architecture design pattern
aimed at separating an application's logic from its UI.

It's mainly made up of three parts:

  1. Model: holds the application's data and business logic.
  2. View: handles the user interface and accepts user input.
  3. ViewModel: acts as the interface between the View and the Model, handling data binding and UI logic.

None of that probably makes much sense yet.
Let's think it through as we go, by building a simple task-tracker app.

🏃‍♀️ Creating the Project

Create the project.

yarn create react-app task-tracker --template typescript

Move into the project folder.

cd task-tracker

Start it up.

yarn start

Just to check, take a look in the browser.

📦 Model

The model defines the application's data structure.
The model handles storing, retrieving, and manipulating data.

We're not using it here, but a model is normally also responsible for
persisting data (e.g., saving it to a database) and
fetching data from external sources (e.g., fetching data from an API).

Create a models folder in the src directory,
and create Task.ts inside it.

export interface Task {
  id: number;
  title: string;
  completed: boolean;
}

🚀 ViewModel

The ViewModel bridges the Model and the View.
It manages the logic for adding tasks, toggling their completed state, and deleting them.

Create TaskViewModel.tsx in the src/models directory

This code defines a hook for managing task state.
The useTaskViewModel hook makes it easy to add tasks, toggle their completed state, and delete them.

import { useState } from 'react';
import { Task } from './Task';

export const useTaskViewModel = () => {
  const [tasks, setTasks] = useState<Task[]>([]);

  const addTask = (title: string) => {
    const newTask: Task = {
      id: Date.now(),
      title,
      completed: false,
    };
    setTasks([...tasks, newTask]);
  };

  const toggleTaskCompletion = (id: number) => {
    setTasks(tasks.map(task =>
      task.id === id ? { ...task, completed: !task.completed } : task
    ));
  };

  const deleteTask = (id: number) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  return {
    tasks,
    addTask,
    toggleTaskCompletion,
    deleteTask,
  };
};

🐏 If we have Model and View, don't we not need ViewModel?

With just a Model and a View, the code gets messy.

  • If the View manipulates the Model directly, logic ends up baked into the View.
    The View then stops being a pure presentation layer, which hurts reusability.
  • Having a ViewModel simply makes testing easier, too — tweaking parameters is much less of a hassle.

👀 View

The View handles the user interface and accepts user interaction.
Here, we'll build the components that display the task list and let you add tasks.

Create TaskList.tsx in the src/components directory

This component displays the task list and handles toggling completion and deleting tasks.

// src/components/TaskList.tsx
import React from 'react';
import { Task } from '../models/Task';

interface TaskListProps {
  tasks: Task[];
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

const TaskList: React.FC<TaskListProps> = ({ tasks, onToggle, onDelete }) => {
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <input
            type="checkbox"
            checked={task.completed}
            onChange={() => onToggle(task.id)}
          />
          {task.title}
          <button onClick={() => onDelete(task.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
};

export default TaskList;

Create AddTask.tsx in the src/components directory

// src/components/AddTask.tsx
import React, { useState } from 'react';

interface AddTaskProps {
  onAdd: (title: string) => void;
}

const AddTask: React.FC<AddTaskProps> = ({ onAdd }) => {
  const [title, setTitle] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (title.trim()) {
      onAdd(title);
      setTitle('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Add a new task"
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default AddTask;

🍳 Wrapping Up

Finally, let's wire these components and the ViewModel together in the App component.

Rewrite App.tsx, the project's base file, as follows.

With this structure, the application's logic (adding tasks, toggling their completed state, deleting them) is concentrated in the ViewModel.

The View can then focus purely on displaying data and receiving user input.
This improves both reusability and testability.

// src/App.tsx
import React from 'react';
import './App.css';
import { useTaskViewModel } from './models/TaskViewModel';
import TaskList from './components/TaskList';
import AddTask from './components/AddTask';

const App: React.FC = () => {
  const { tasks, addTask, toggleTaskCompletion, deleteTask } = useTaskViewModel();

  return (
    <div className="App">
      <h1>Task Tracker</h1>
      <AddTask onAdd={addTask} />
      <TaskList
        tasks={tasks}
        onToggle={toggleTaskCompletion}
        onDelete={deleteTask}
      />
    </div>
  );
};

export default App;

And that's it!
This should be all you need!!