Skip to content

MS_ASPNETCoreReactTemplate

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

ASP.NET Core React.jsテンプレート

概要

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/

JavaScript Services も流行ってないので削除しました。
 IDE に標準のテンプレートとして表示されなくなっている(dotnet new コマンドには残っている)。

補足(サンプルは削除されたが、解説は今も読む価値がある): サンプル
コード自体は削除されているが、
本ページの「詳細」節は React の構造を丁寧に追っており、
教材として今も有効
である。

【今も通用する部分】
   ・エントリ ポイント → ルーティング → コンポーネント という階層
   ・state と再描画の関係
   ・API 呼び出しと loading 状態の扱い

【古くなった部分】
   ・webpack 前提(現在は Vite)
   ・クラス コンポーネント(現在は関数コンポーネント + Hooks)★
   ・react-router v4 の書き方(現在は v6)
   ・isomorphic-fetch(現在はブラウザ標準の fetch)
   ・ASP.NET Core の Razor View が土台(現在は index.html)

各節で、現在の書き方を併記する。

構成

フォルダ

ReactTemplate
 ├ ClientApp
 |  ├ components
 |  |  ├ Counter.tsx
 |  |  ├ FetchData.tsx
 |  |  ├ Home.tsx
 |  |  ├ Layout.tsx
 |  |  └ NavMenu.tsx
 |  ├ css
 |  |  └ site.css
 |  ├ boot.tsx
 |  └ routes.tsx
 ├ Controllers
 |  ├ HomeController.cs
 |  └ SampleDataController.cs
 ├ Views
 |  ├ Home
 |  |  └ Index.cshtml
 |  ├ Shared
 |  |  ├ _Layout.cshtml
 |  |  └ Error.cshtml
 |  ├ _ViewImports.cshtml
 |  └ _ViewStart.cshtml
 ├ wwwroot
 |  └ favicon.ico
 ├ .gitignore
 ├ appsettings.Development.json
 ├ appsettings.json
 ├ npm-shrinkwrap.json
 ├ package.json
 ├ Program.cs
 ├ ReactTemplate.csproj
 ├ Startup.cs
 ├ tsconfig.json
 ├ webpack.config.js
 └ webpack.config.vendor.js

補足(この構成が示す設計と、現在の構成): 1 つのプロジェクトに
.NET と React が同居している
——これが当時のテンプレートの特徴である。

【当時(このテンプレート)】
   ReactTemplate.csproj  ← 1 つのプロジェクト
     ├ Controllers / Views  … .NET 側
     ├ ClientApp            … React 側
     └ package.json         … npm も同じ階層

   ・dotnet build すると webpack も走る(MSBuild のターゲットで連携)
   ・【1 つのプロジェクトで完結する】= 当時の売り

【現在(.NET 8 以降のテンプレート)】
   MyApp.sln
     ├ MyApp.Server/        … ASP.NET Core(Web API のみ)
     └ myapp.client/        … 【React(.esproj)】★
          ├ package.json
          ├ vite.config.ts
          └ src/

   ・【2 つのプロジェクトに分離された】
   ・[.esproj(JavaScript・TypeScriptプロジェクトシステム)] 参照

この変化が、ASP.NET Core SPAテンプレート
原文が主張していた「フロントとバックを分離せよ」の実現
である。

npm-shrinkwrap.json について:
現在は package-lock.json が標準である
(npm 5 以降。shrinkwrap は明示的に発行する場合のみ)。
どちらも依存の版を固定するためのファイルで、
必ずソース管理に含める
NuGetパッケージの依存関係の肥大化 と同じ問題意識)。

スタック

Node.js、React、TypeScript、webpack からなる。

  • Node.js(DNET_NodeJs.md
  • React(DNET_React.md
  • TypeScript
  • webpack(DNET_Webpack.md

補足(各要素の役割と現況):

要素 役割 現況
Node.js ビルド ツールの実行環境(本番では不要) 現役。LTS 版を使う
React UI ライブラリ 現役。ただし書き方が変わった(Hooks)
TypeScript 静的型付け 現役。むしろ標準化した
webpack モジュール バンドラ Vite に置き換わりつつある
【webpack → Vite への移行の理由】
   ・開発サーバの起動が【桁違いに速い】
      webpack: 全部バンドルしてから起動(大規模で数十秒)
      Vite:    ESM をそのまま配信、変更分だけ変換(1 秒未満)
   ・設定が単純(webpack.config.js の複雑さが有名だった)
   ・本番ビルドは Rollup(実績のあるバンドラ)

原文の構成に webpack.config.jswebpack.config.vendor.js
2 つがある
のは、ベンダー ライブラリ(React 本体など)と
アプリ コードを別々にバンドル
するためである。

【当時の工夫】
   vendor(React, react-dom 等)… ほぼ変わらない → 長期キャッシュ
   app(自分のコード)           … 頻繁に変わる
     → 分けることで、アプリ更新時に vendor を再取得させない

【現在】 Vite / webpack 5 が【自動でコード分割する】ため、
         手で分ける必要はほぼない

詳細

Index.cshtml(土台)

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/Views/Home/Index.cshtml

補足(Razor View を土台にする方式の意味): 現在の SPA は
静的な index.html を土台にするのが普通だが、
当時は Razor View(.cshtml)を土台にしていた

【Razor を土台にする利点(当時)】
   ・サーバ側の値を初期状態として埋め込める
       <div id="react-app" data-user="@User.Identity.Name">
   ・認証・認可を .NET 側で掛けられる
   ・asp-append-version でキャッシュ バスティングが効く ★

【欠点】
   ・フロントを独立して配信できない(CDN に置けない)
   ・.NET を起動しないとフロントが見られない
   ・フロント担当者が .NET の知識を要求される

asp-append-version="true"
ASP.NET の BundleConfig で述べた
キャッシュ バスティングである。

<script src="~/dist/main.js" asp-append-version="true"></script>
<!-- → /dist/main.js?v=Ynv7Ur0kEz…(内容のハッシュ) -->

現在は Vite がファイル名にハッシュを埋め込むため、
この機能は不要になった。

dist/assets/index-4f8a2b1c.js   ← ファイル名自体にハッシュ

<div id="react-app">Loading...</div>Loading... は、
JS が読み込まれるまでの間に表示される文字である。
現在も同じ手法が使われる(あるいはスケルトン UI やスピナー)。

boot.tsx(EntryPoint)

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/boot.tsx

「Index.cshtml(土台)」の「<div id="react-app">」の部分に、
routes のコンテンツをロードする。

import

Node.js のモジュールのインポート

  • CSS

    import './css/site.css';
  • ホットロード
    AppContainer タグは、React 拡張プロダクトの独自タグ。

    { AppContainer } from 'react-hot-loader';
  • URL と DOM を仮想的に紐付け(HTML5 History を内部で使用)。
    BrowserRouter タグは、React 拡張プロダクトの独自タグ。

    { BrowserRouter } from 'react-router-dom';

renderApp

  • ReactDOM.render メソッドを使用して、document.getElementById('react-app') で
    Index.cshtml の <div id="react-app"> 要素に AppContainer と、
    BrowserRouter を適用して、描画する。
function renderApp() {
    // This code starts up the React app when it runs in a browser. It sets up the routing
    // configuration and injects the app into a DOM element.
    const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!;
    ReactDOM.render(
        <AppContainer>
            <BrowserRouter children={ routes } basename={ baseUrl } />
        </AppContainer>,
        document.getElementById('react-app')
    );
}

補足(現在の書き方): この部分はAPI が変わったので、
対応を示しておく。

// 現在(React 18 以降 + react-router v6 + Vite)
import React from 'react';
import { createRoot } from 'react-dom/client';   // ← ReactDOM.render は非推奨
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './css/site.css';

const container = document.getElementById('react-app')!;
createRoot(container).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
当時 現在
ReactDOM.render(...) createRoot(container).render(...)(React 18)
react-hot-loaderAppContainer 不要(Vite の HMR が標準で効く)
<BrowserRouter children={routes} /> <BrowserRouter><App /></BrowserRouter>
basename<base> タグから取得 basename を直接指定(または不要)

<base href> からベース URL を取るという当時の工夫は、
アプリが仮想ディレクトリ配下に配置される場合への対応である
/myapp/ に置かれてもルーティングが壊れないようにする)。
現在は Vite の base 設定で同じことを行う。

// vite.config.ts
export default defineConfig({ base: '/myapp/' });

document.getElementsByTagName('base')[0]!(非 null アサーション)
は TypeScript の構文である。
「null ではないと保証する」という宣言であり、
実際に null なら実行時に落ちる
多用すると型チェックの意味が薄れるため、
現在はオプショナル チェーンや早期リターンを使う方が良い。

routes.tsx(URLとDOMの紐付け)

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/routes.tsx

概要

  • 各種コンポーネントをインポート

  • ReactRouter の定義

export const routes = <Layout>
    <Route exact path='/' component={ Home } />
    <Route path='/counter' component={ Counter } />
    <Route path='/fetchdata' component={ FetchData } />
</Layout>;

表示

  • 初期表示時の描画

    • <Layout></Layout> → Layout.tsx
  • 以降の描画(URL に依存)

    • <Route exact path='/' component={ Home } />(Home.tsx、exact は初期表示)
    • <Route path='/counter' component={ Counter } />(Counter.tsx)
    • <Route path='/fetchdata' component={ FetchData } />(FetchData.tsx)

移行メモ(exact の意味): 原文は「exact は初期表示」と
説明しているが、正確には「パスの完全一致」を要求する指定である。

【exact なし】 path='/' は【前方一致】する
   /          → 一致 ○
   /counter   → 一致 ○("/" で始まるため)★ 意図しない
   → Home と Counter が【両方表示されてしまう】

【exact あり】 完全一致のみ
   /          → 一致 ○
   /counter   → 一致 ✗

結果として「初期表示(ルート)のときだけ Home が出る」という
挙動になるため、原文の説明も的は外していないが、
**仕組みは「完全一致の指定」**である。

現在(react-router v6)では exact は廃止された
既定で完全一致になり、前方一致したい場合に /* を付ける。

// react-router v6
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />              {/* exact 相当 */}
    <Route path="counter" element={<Counter />} />
    <Route path="fetchdata" element={<FetchData />} />
  </Route>
</Routes>
当時(v4) 現在(v6)
<Route component={X} /> <Route element={<X />} />
exact 既定で完全一致(不要)
<Layout> で囲む 入れ子ルート + <Outlet />
this.props.children <Outlet />

各コンポーネント

Layout.tsx

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/components/Layout.tsx

  • 静的なレイアウト用の HTML を返す "public render() {" だけ実装される。

  • 初期表示時の描画

    • <NavMenu/> → NavMenu.tsx
    • { this.props.children } → this.props.children で routes(ReactRouter)に
      定義したコンテンツをロードする。

NavMenu.tsx

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/components/NavMenu.tsx

  • ナビゲーション用の静的な HTML を返す "public render() {" だけ実装される。
  • <Link>, <NavLink> コンポーネントを使用している。

補足(<Link> を使う理由): これが SPA の要なので補っておく。

<!-- ✗ 普通のリンク → ページ全体が再読込される(SPA の意味がない) -->
<a href="/counter">Counter</a>

<!-- ○ Link → JS がルーティングを処理。再読込しない -->
<Link to="/counter">Counter</Link>
【Link がやっていること】
   ① クリックの既定動作を止める(preventDefault)
   ② history.pushState で【URL だけ書き換える】(HTML5 History API)
   ③ Router に通知 → 対応するコンポーネントを描画

   → サーバへのリクエストが【発生しない】= 速い
   → URL は変わるので、戻るボタン・ブックマークが効く ★

<NavLink><Link> の拡張で、
現在の URL と一致するときにクラスを付ける
(メニューの「現在地」表示に使う)。

これが Spa Services で述べた
MapFallbackToFile が必要になる理由
でもある。
URL が /counter に変わった状態で再読込されると、
サーバに /counter が飛ぶ
ためである。

Home.tsx

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/components/Home.tsx

  • ホーム画面用の静的な HTML を返す "public render() {" だけ実装される。

Counter.tsx

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/components/Counter.tsx

カウントアップ処理のテスト画面の HTML を返す。

  • constructor() {
    オブザーバーが constructor で初期化した this.state を監視して再描画。

  • public render() {

    • onClick イベントは JSX で書いてある。ここで、incrementCounter メソッドを呼び出す。
    • incrementCounter メソッドが this.setState を呼び this.state を更新すると、再描画される。

補足(state と再描画:React の中核概念): 原文の説明は
要点を押さえているが、「オブザーバーが監視して」という表現は
少し補足が要る。

【正確な仕組み】
   React は state を【監視していない】
   setState を【呼んだこと】が再描画のきっかけになる ★

   ✗ this.state.count = 5;       ← 直接書き換えても再描画されない
   ○ this.setState({ count: 5 }); ← これで再描画される

【流れ】
   setState 呼び出し
     → React が「この コンポーネントは古い」と印を付ける
     → 【仮想 DOM】を再構築
     → 前回の仮想 DOM と差分を取る(Reconciliation)
     → 【差分だけ】実 DOM に反映する

**これが「宣言的 UI」**である。

【命令的(jQuery 的)】
   $('#count').text(count + 1);   ← DOM を直接操作する

【宣言的(React)】
   「state が count のとき、画面はこう見える」と書く
     → どう更新するかは React が決める
     → [ASP.NET Web Forms] の ViewState/ポストバックとも
       発想が違う(あちらはサーバが HTML を作り直す)

現在の書き方(関数コンポーネント + Hooks):

// クラス コンポーネント(当時)
export class Counter extends React.Component<{}, { currentCount: number }> {
    constructor(props) { super(props); this.state = { currentCount: 0 }; }
    render() {
        return <button onClick={() => this.incrementCounter()}>Increment</button>;
    }
    incrementCounter() { this.setState({ currentCount: this.state.currentCount + 1 }); }
}

// 関数コンポーネント + useState(現在)★
export function Counter() {
    const [count, setCount] = useState(0);
    return (
      <>
        <p>Current count: {count}</p>
        <button onClick={() => setCount(c => c + 1)}>Increment</button>
      </>
    );
}

setCount(c => c + 1) と書く理由:

・setState / setCount は【非同期にまとめて処理される】ことがある
・setCount(count + 1) を連続で呼ぶと、古い count を参照して
  1 しか増えない場合がある
・【関数形式】なら、常に最新の値を受け取れる ★

React 16.8(2019 年)で Hooks が導入されて以降、
新規はすべて関数コンポーネント
である。
クラス コンポーネントは今も動くが、
新しい機能(Suspense、並行機能)は Hooks 前提で作られている。

FetchData.tsx

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Template/SPATemplate/ReactTemplate/ClientApp/components/FetchData.tsx

WebAPI からデータを取得しリスト表示する処理のテスト画面の HTML を返す。

  • constructor() {

    • constructor で this.state を初期化
    • isomorphic-fetch による fetch は WebAPI 呼び出し。
    • WebAPI から取得したデータを this.setState
      • forecasts: リストに表示するデータ
      • loading: loading 中か否か。
  • public render() {
    this.state.loading を判定して描画を変えている。

    • true:Loading... の表示

      <p><em>Loading...</em></p>
    • false:
      renderForecastsTable メソッドが返したリストの内容。

補足(isomorphic-fetch とは、そして現在): 名前が特徴的なので
説明しておく。

【isomorphic(同型)とは】
   「ブラウザでもサーバ(Node.js)でも【同じコードが動く】」という意味

   ・ブラウザ … window.fetch が標準で存在する
   ・Node.js  … 当時は fetch がなかった(node-fetch 等が必要)
     → isomorphic-fetch がその差を吸収する

【なぜ必要だったか】
   サーバー サイド レンダリング(SSR)で、
   同じコンポーネントを Node.js 上でも実行するため ★
     → [ASP.NET Core React+Reduxテンプレート] の boot-server.tsx

現在は不要である。

・ブラウザ … fetch は全ブラウザで標準(IE 終了)
・Node.js  … 【Node 18 以降で fetch が標準搭載】★
  → isomorphic-fetch を入れる理由がなくなった

現在の書き方:

export function FetchData() {
    const [forecasts, setForecasts] = useState<Forecast[]>([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        const ac = new AbortController();               // ← 中断できるようにする
        (async () => {
            try {
                const res = await fetch('/api/SampleData/WeatherForecasts',
                                        { signal: ac.signal });
                if (!res.ok) throw new Error(`HTTP ${res.status}`);  // ★ 重要
                setForecasts(await res.json());
            } catch (e) {
                if ((e as Error).name !== 'AbortError') setError(String(e));
            } finally {
                setLoading(false);
            }
        })();
        return () => ac.abort();                        // ← 後始末
    }, []);

    if (loading) return <p><em>Loading...</em></p>;
    if (error)   return <p role="alert">エラー: {error}</p>;
    return <ForecastTable forecasts={forecasts} />;
}

原文のサンプルに欠けている 3 点(現在なら必ず入れる):

① 【エラー処理】
     fetch は【HTTP 404 / 500 でも例外を投げない】★
     → res.ok を必ず確認する(C# の HttpClient と違う点)

② 【中断(クリーンアップ)】
     応答が返る前に画面を離れると、
     アンマウント済みのコンポーネントに setState する
     → 警告、あるいはメモリ リーク

③ 【loading / error / データ の 3 状態】
     原文は loading の 2 状態しかない
     → 実務では「失敗した」状態の表示が必ず要る

さらに現在は、データ取得を自前で書かずに
ライブラリに任せる
のが主流である。

ライブラリ 内容
TanStack Query(React Query) キャッシュ・再取得・リトライ・重複排除を全部やる ★
SWR 同種。より軽量
RTK Query Redux Toolkit に統合されたもの
// TanStack Query なら、これだけ
const { data, isLoading, error } = useQuery({
    queryKey: ['forecasts'],
    queryFn: () => fetch('/api/SampleData/WeatherForecasts').then(r => r.json()),
});

参考

開発基盤部会 Wiki

JavaScript - React

  • JavaScript(DNET_JavaScript.md
    • React(DNET_React.md

この処理を、create-react-appで作成し、掘り下げを行う。

React のセカンド・ステップ(DNET_ReactSecondStep.md

移行メモ(create-react-app は非推奨になった): 参考リンクが挙げる
create-react-app(CRA)は、React 公式から非推奨とされた
(2023 年に公式ドキュメントから外れ、実質的にメンテナンス停止)。

【現在の推奨(React 公式)】
   ・Vite            … 【SPA を作るならこれ】★
   ・Next.js         … SSR / SSG が要るなら
   ・Remix           … 同上
   ・Astro           … コンテンツ主体のサイト
# 現在の作り方
npm create vite@latest my-app -- --template react-ts

Microsoft Learn


Tags: 移行, .NET開発, .NET Core, ASP.NET, ASP.NET Web API, ASP.NET SPA, JavaScript

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally