今回は、Visual Studio 2026 と最新の ASP.NET Core + React テンプレートを使って、Todoアプリを作る方法を解説します。
以前の記事で使っていたReact + ASP.NET Coreテンプレートが非推奨となったため、最新の推奨構成で最初から作り直す形で手順をまとめました。
この記事で作るもの
まずは完成イメージを共有します。今回は、Todoを追加できて、完了ボタンで状態を切り替えられるだけのシンプルなアプリを作ります。

画面の機能は次の2つだけです。
- Todoを追加する
- Todoを完了状態にする
データベースは使わず、サーバー側のメモリ上に保持する形で進めます。まずは ASP.NET Core と React の連携を最短で確認するのが目的です。
開発環境
ここから実際に作業していきます。まずは今回使う環境をそろえます。
記事執筆時点の検証環境は次のとおりです。
- .NET 10
- Visual Studio 2026(Visual Studio 2022のバージョン17.8以降でも可)
- React と ASP.NET Coreテンプレート
- React 19 系
- TypeScript 5 系
- Node.js 24 系
もしNode.jsを入れていない場合は、下記サイトからインストールしてください。バージョンはLTSと付いている最新のものを選択してください。

また、Visual Studio 2022または2026で「React と ASP.NET Coreテンプレート」を使うためには、「ASP.NET と Web 開発」ワークロードを入れておく必要があります。

VSCodeでも dotnet new react を実行することで ASP.NET Core + React プロジェクトを作成できますが、本記事では最短で再現しやすい Visual Studio ベースの手順を採用しています。
プロジェクトを作成する
環境がそろったら、まずはプロジェクトを作成します。
Visual Studio で「新しいプロジェクトの作成」を開き、「React」と検索すると、「ReactとASP.NET Core」のテンプレートが表示されるので、「TypeScript」と表示されているものを選択します。

プロジェクト名は、何でも構いませんが、今回はTodoReactAppにしておきます。
以下の設定はデフォルトのままでOKです。

プロジェクトが作成されたら、TodoReactApp.Serverのプロジェクト名を右クリックし、「スタートアッププロジェクトに設定」を選択してください。

これでServer(バックエンド)→client(フロントエンド)の順番でアプリが起動するようになります。
また、この設定は必須ではないですが、デバッグ実行時に自動でブラウザが起動するように、Server側のProperties>launchSettings.jsonのlaunchBrowserの設定をtrueにしておきます。

プロジェクト起動時に「接続がプライベートではありません」というエラー画面が表示される場合は、コマンドプロンプト等で dotnet dev-certs https --trust を実行して、証明書をインストールしてください。

ここで一度、ソリューション構成を軽く確認しておきます。今回は細かい説明は省きますが、ASP.NET Core 側と React 側が1つのソリューション内にまとまっている構成になっているはずです。
ASP.NET Core(バックエンド)側に Todo API を作成する
プロジェクトが作成できたら、次はバックエンド(Server)側に Todo API を作ります。
今回は、一覧取得・追加・完了更新の3つだけ実装します。
まず、Modelsフォルダを作成し、TodoItem.cs を作成してください。
namespace TodoReactApp.Server.Models;
public class TodoItem
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsCompleted { get; set; }
}次に、ControllersフォルダにTodoController.cs を作成します。一覧取得・追加・完了フラグ更新の3つのエンドポイントを持ちます。
using Microsoft.AspNetCore.Mvc;
using TodoReactApp.Server.Models;
namespace TodoReactApp.Server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
/// <summary>
/// Todo データを保持します。実際のアプリではデータベースから取得しますが、今回はサンプルのためメモリ上のリストで簡略化しています。
/// </summary>
private static readonly List<TodoItem> Todos =
[
new TodoItem { Id = 1, Title = "ASP.NET Core APIを作る", IsCompleted = false },
new TodoItem { Id = 2, Title = "Reactから一覧を表示する", IsCompleted = true }
];
/// <summary>
/// Todo 一覧を取得します。
/// </summary>
[HttpGet]
public ActionResult<IEnumerable<TodoItem>> Get()
{
return Ok(Todos.OrderBy(x => x.Id));
}
/// <summary>
/// 新しい Todo を追加します。
/// </summary>
[HttpPost]
public ActionResult<TodoItem> Create(CreateTodoRequest request)
{
if (string.IsNullOrWhiteSpace(request.Title))
{
return BadRequest("タイトルは必須です。");
}
var newTodo = new TodoItem
{
Id = Todos.Count == 0 ? 1 : Todos.Max(x => x.Id) + 1,
Title = request.Title,
IsCompleted = false
};
Todos.Add(newTodo);
return Ok(newTodo);
}
/// <summary>
/// 指定した Todo を完了状態にします。
/// </summary>
[HttpPut("{id}/complete")]
public IActionResult Complete(int id)
{
var todo = Todos.FirstOrDefault(x => x.Id == id);
if (todo is null)
{
return NotFound();
}
todo.IsCompleted = true;
return NoContent();
}
}
public class CreateTodoRequest
{
public string Title { get; set; } = "";
}実装できたら、API 単体で動作確認をしておきます。
Visual Studio で作成した ASP.NET Core プロジェクトには、最初から .http ファイルが含まれており、エディタ上から API リクエストを送信できます。
今回は TodoReactApp.Server.http に Todo API 用のリクエストを追加し、一覧取得・追加・完了更新が正しく動くかを確認します。
TodoReactApp.Server.httpを以下のように書き換えてください。
@TodoReactApp.Server_HostAddress = http://localhost:5279
### Todo一覧を取得
GET {{TodoReactApp.Server_HostAddress}}/api/todo
Accept: application/json
###
### Todoを追加
POST {{TodoReactApp.Server_HostAddress}}/api/todo
Content-Type: application/json
{
"title": "React画面を作成する"
}
###
### Todoを完了にする
PUT {{TodoReactApp.Server_HostAddress}}/api/todo/1/complete
###デバッグ実行し、サーバーが起動した状態で .http ファイルの「要求の送信」をクリックすると API を実行できます。

なお、環境によってポート番号が異なる場合があります。
その場合は、launchSettings.json で実際の ASP.NET Core 側のポート番号を確認し、TodoReactApp.Server.httpの1行目のポート番号を変更してください。
React(フロントエンド)側にTodo一覧画面を作成する
ここからフロントエンドを実装します。
今回はReact側でTodo一覧を表示し、入力欄から新しいTodoを追加し、完了ボタンで状態を更新できるようにします。
まず、src/App.tsx を次の内容に置き換えてください。
import { useEffect, useState } from 'react';
import './App.css';
interface TodoItem {
id: number;
title: string;
isCompleted: boolean;
}
function App() {
// Todo 一覧を保持する state
const [todos, setTodos] = useState<TodoItem[]>();
// 入力欄の値を保持する state
const [newTitle, setNewTitle] = useState('');
useEffect(() => {
// 画面の初回表示時に Todo 一覧を取得する
populateTodoData();
}, []);
const contents = todos === undefined
? <p><em>Loading... Please refresh once the ASP.NET backend has started.</em></p>
: <div>
<div className="todo-form">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="やることを入力"
className="todo-input"
/>
<button onClick={addTodo} className="add-button">追加</button>
</div>
<ul className="todo-list">
{todos.map(todo =>
<li key={todo.id} className="todo-item">
<span className={`todo-text ${todo.isCompleted ? 'completed' : ''}`}>
{todo.title}
</span>
{!todo.isCompleted &&
<button
onClick={() => completeTodo(todo.id)}
className="complete-button">
完了
</button>
}
</li>
)}
</ul>
</div>;
return (
<div className="container">
<h1 className="page-title">Todo App</h1>
<p className="description">
ASP.NET Core Web API と React を使ったシンプルな Todo アプリです。
</p>
{contents}
</div>
);
async function populateTodoData() {
// Todo 一覧を取得する
const response = await fetch('/api/todo');
if (response.ok) {
const data = await response.json();
setTodos(data);
}
}
async function addTodo() {
if (!newTitle.trim()) {
return;
}
// 入力されたタイトルを API に送信して Todo を追加する
const response = await fetch('/api/todo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: newTitle })
});
if (response.ok) {
setNewTitle('');
await populateTodoData();
}
}
async function completeTodo(id: number) {
// 指定した Todo を完了状態に更新する
const response = await fetch(`/api/todo/${id}/complete`, {
method: 'PUT'
});
if (response.ok) {
await populateTodoData();
}
}
}
export default App;このコードでやっていることはシンプルです。
初回表示時に Todo 一覧を取得し、追加ボタンで POST /api/todo を呼び出し、完了ボタンで PUT /api/todo/{id}/complete を呼び出しています。
次に、スタイルが何もないと味気ないので、最低限の CSS を当てます。
src/App.css を次の内容に置き換えてください。
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background-color: #f5f7fb;
color: #222;
}
* {
box-sizing: border-box;
}
#root {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 48px 16px;
}
.container {
width: 100%;
max-width: 720px;
background-color: #ffffff;
padding: 32px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.page-title {
margin-top: 0;
margin-bottom: 12px;
font-size: 32px;
font-weight: 700;
line-height: 1.2;
}
.description {
margin: 0 0 24px;
font-size: 16px;
line-height: 1.7;
color: #444;
}
.todo-form {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.todo-input {
flex: 1;
padding: 12px 14px;
border: 1px solid #d6dbe6;
border-radius: 10px;
font-size: 16px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.todo-input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.add-button,
.complete-button {
border: none;
border-radius: 10px;
padding: 10px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
}
.add-button:hover,
.complete-button:hover {
opacity: 0.9;
}
.add-button:active,
.complete-button:active {
transform: translateY(1px);
}
.add-button {
background-color: #2563eb;
color: #ffffff;
white-space: nowrap;
}
.complete-button {
background-color: #10b981;
color: #ffffff;
white-space: nowrap;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid #eef2f7;
}
.todo-text {
flex: 1;
font-size: 16px;
line-height: 1.6;
word-break: break-word;
}
.completed {
color: #94a3b8;
text-decoration: line-through;
}
.loading-text {
margin: 0;
color: #555;
line-height: 1.7;
}
.error-message {
margin: 0;
color: #dc2626;
line-height: 1.7;
}
@media (max-width: 640px) {
.container {
padding: 24px;
}
.todo-form {
flex-direction: column;
}
.add-button,
.complete-button {
width: 100%;
}
.todo-item {
flex-direction: column;
align-items: stretch;
}
}これで、最低限見やすい Todo 画面になります。
最後に、設定ファイルを修正します。
テンプレート作成直後の vite.config.ts では、サンプルAPIである /weatherforecast へのアクセスを ASP.NET Core 側へ転送する設定になっています。
今回は Todo API を /api/todo で公開するため、proxyの箇所を以下に変更してください。
server: {
proxy: {
'^/api': {
target,
secure: false
}
},
port: parseInt(env.DEV_SERVER_PORT || '65236'),
https: {
key: fs.readFileSync(keyFilePath),
cert: fs.readFileSync(certFilePath),
}
}実際に動かして確認する
ここまでできたら、最後に動作確認をします。まずはアプリを起動し、初期データが一覧表示されることを確認してください。

次に、入力欄に新しい Todo を入れて「追加」を押し、一覧に反映されることを確認します。

続いて、未完了の Todo の「完了」ボタンを押し、文字に取り消し線が付き、ボタンが消えることを確認してください。

まとめ
今回は、Visual Studio 2026 と最新の ASP.NET Core + React テンプレートを使って、Todo の追加と完了ができる最小アプリを作成しました。
この記事の目的は、最新テンプレートでの開発手順をできるだけ短時間で一通り体験することでした。
この先は、削除機能の追加、データベース連携、バリデーションの実装などに進めていくと、より実践的なアプリになっていきます。まずは今回の内容をベースに、手を動かしながら少しずつ発展させてみてください。