Skip to content

MS_ASPNETCoreReactReduxTemplate

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

ASP.NET Core React+Reduxテンプレート

概要

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

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

構成

フォルダ

ReactReduxTemplate
 ├ ClientApp
 |  ├ components
 |  |  ├ Counter.tsx
 |  |  ├ FetchData.tsx
 |  |  ├ Home.tsx
 |  |  ├ Layout.tsx
 |  |  └ NavMenu.tsx
 |  ├ css
 |  |  └ site.css
 |  ├ store
 |  |  ├ Counter.ts
 |  |  ├ index.ts
 |  |  └ WeatherForecasts.ts
 |  ├ boot-client.tsx
 |  ├ boot-server.tsx
 |  ├ configureStore.ts
 |  └ 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
 ├ ReactReduxTemplate.csproj
 ├ Startup.cs
 ├ tsconfig.json
 ├ webpack.config.js
 └ webpack.config.vendor.js

スタック

ASP.NET Core React.jsテンプレート
Redux(DNET_Redux.md)が追加されている。

補足(Redux とは何を解決するものか): 「React に Redux を足す」
という話の動機を押さえておく。

【React だけの場合の問題】

   App
    ├ Header ─ ユーザー名を表示したい
    ├ Sidebar
    └ Main
         └ Content
              └ Form ─ ログイン処理を行う

   Form の結果を Header に反映したい
     → 状態を【共通の祖先(App)】に持ち上げる(lifting state up)
     → props で【何段も下まで渡す】(prop drilling)★ 苦痛
【Redux の解】
   状態を【1 箇所(Store)】に集め、
   どのコンポーネントからも直接読める/更新できるようにする

      Store(唯一の状態)
        ▲       │
        │Action │state
        │       ▼
   ┌────┴───────────┐
   Header   Form   Content …

Redux の 3 原則:

 ① Single source of truth  … 状態は【1 つの Store】に集約する
 ② State is read-only      … 状態は直接書き換えず、【Action を投げる】
 ③ Changes with pure functions … Reducer は【純粋関数】
                                  (state, action) => 新しい state

(state, action) => newState という形は、
C# の Aggregate(畳み込み)と同じ発想である。
副作用がないため、同じ Action の列を再生すれば同じ状態になる
——これが Time Travel Debugging を可能にしている。

詳細

ASP.NET Core React.jsテンプレート からの変更点

追加

以下が追加されている。

├ store
|  ├ Counter.ts
|  ├ index.ts
|  └ WeatherForecasts.ts
├ boot-client.tsx
├ boot-server.tsx
├ configureStore.ts

store

TypeScript なので、Reducer ではなく Bean 的に Store として定義するらしい。
(実際は、ココで、Reducer、Action、Store が定義される)

  • store フォルダの追加

    store
     ├ Counter.ts
     ├ index.ts
     └ WeatherForecasts.ts
    
    • 1 つの JSON ではなく複数の Bean として定義してある。
    • Store、Action、Reducer の定義
    • 非同期処理は Reducer に書かれるのでコチラ。
  • configureStore.ts ファイルの追加

    • store の設定
    • reducer を Hot Reloading で構築

補足(この構成は「Ducks パターン」である): 原文が「Bean 的に」と
表現している構成には、名前が付いている

【従来の Redux(機能ごとにファイルを分ける)】
   actions/counter.ts      … Action Creator
   reducers/counter.ts     … Reducer
   constants/counter.ts    … Action Type の定数
     → 1 つの機能を追加するのに【3 ファイル触る】★ 面倒

【Ducks パターン(機能ごとに 1 ファイルにまとめる)】
   store/Counter.ts        … Action Type + Action Creator + Reducer + 型
     → 【関連するものが 1 箇所】にある
     → 原文の構成はこちら

index.ts の役割は、各 Reducer を結合することである
combineReducers)。

// store/index.ts
export interface ApplicationState {
    counter: Counter.CounterState;
    weatherForecasts: WeatherForecasts.WeatherForecastsState;
}
export const reducers = {
    counter: Counter.reducer,
    weatherForecasts: WeatherForecasts.reducer,
};

移行メモ(「非同期処理は Reducer に書かれる」は正確でない): 原文の
この記述は、Redux の原則に照らすと逆である。

【Redux の原則】
   Reducer は【純粋関数】でなければならない
     → 同じ入力なら常に同じ出力
     → 【副作用(API 呼び出し)を書いてはならない】★

【では非同期はどこに書くのか】
   ミドルウェア(redux-thunk)を通した
   【Action Creator】に書く
// store/WeatherForecasts.ts(Ducks なので同じファイルにある)
export const actionCreators = {
    // ↓ これは「関数を返す Action Creator」= thunk
    requestWeatherForecasts: (startDateIndex: number) =>
        async (dispatch, getState) => {
            dispatch({ type: 'REQUEST_WEATHER_FORECASTS', startDateIndex });
            const res = await fetch(`/api/SampleData/WeatherForecasts`);
            const forecasts = await res.json();
            dispatch({ type: 'RECEIVE_WEATHER_FORECASTS', forecasts });
        }
};

// Reducer は純粋なまま(受け取った Action で state を作り直すだけ)
export const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'REQUEST_WEATHER_FORECASTS': return { ...state, isLoading: true };
        case 'RECEIVE_WEATHER_FORECASTS': return { ...state, isLoading: false,
                                                   forecasts: action.forecasts };
        default: return state;
    }
};

原文の構成では両方が同じファイル(store/*.ts)にあるため、
「Reducer に書かれる」=「store フォルダのファイルに書かれる」
という意味だった可能性が高い。場所としては原文の通りである。

boot

  • boot-client.tsx の追加
    boot に対応。

  • boot-server.tsx の追加
    サーバーサイド・レンダリングという技術を
    利用するのために実装されているらしいが詳細不明。

補足(「詳細不明」への回答:SSR とは): **サーバー サイド
レンダリング(SSR)**の仕組みを補っておく。

【CSR(クライアント サイド レンダリング)= 通常の SPA】
   ブラウザ → サーバ: GET /
   サーバ  → ブラウザ: <div id="react-app">Loading...</div> + JS
   ブラウザ: JS をダウンロード → 実行 → API 呼び出し → 描画
     → 【最初は空白】。表示までに時間がかかる
     → 検索エンジンが中身を読めない場合がある

【SSR(サーバー サイド レンダリング)】
   ブラウザ → サーバ: GET /
   サーバ: 【Node.js 上で React を実行】し、HTML を組み立てる
   サーバ  → ブラウザ: 完成した HTML + JS
   ブラウザ: すぐ表示できる。その後 JS が「水和(hydrate)」して
             インタラクティブになる ★

boot-client.tsxboot-server.tsx に分かれる理由:

boot-client.tsx … ブラウザで動く。hydrate する
boot-server.tsx … 【Node.js で動く】。HTML 文字列を返す

  → 同じコンポーネントを、2 つの環境で実行する
  → だから【isomorphic】(同型)という語が出てくる
    ([ASP.NET Core React.jsテンプレート] の isomorphic-fetch)

なぜ ASP.NET Core でこれが廃れたのか:

【本番環境で Node.js プロセスを抱えることになる】
   ・.NET のプロセスから Node を起動して面倒を見る
   ・スケール、監視、障害対応の対象が 2 倍になる
   ・[JavaScript Services] が廃止された理由そのもの ★

【現在の解】
   SSR が要るなら、【Node.js 側で完結させる】
     → Next.js / Remix / Nuxt / Angular Universal
     → .NET は【API を提供するだけ】に徹する

configureStore.ts が client/server 両方から使われるのも
この構成のためで、Store を共有できる形にする必要があった
(サーバで作った初期状態を HTML に埋め込み、クライアントで復元する)。

差分

package

redux と、redux-thunk の導入により、
既存の WebAPI 呼び出しの非同期処理を行う、
isomorphic-fetch と、json-loader が削除されている。

  • 追加

    • redux

    • redux-thunk

    • react-redux

    • react-router-redux

    • node-noop

    • history

    • domain-task

  • 削除

    • isomorphic-fetch
    • json-loader

補足(各パッケージの役割と現況):

パッケージ 役割 現況
redux Store / Reducer の中核 現役。ただし Redux Toolkit 経由が推奨
redux-thunk 非同期の Action を可能にする Redux Toolkit に同梱された
react-redux React と Redux を繋ぐ(connect / Hooks) 現役
react-router-redux ルーティング状態を Store に入れる 廃止connected-react-router も現在は非推奨)
node-noop Node 環境で何もしないダミー 不要
history HTML5 History の抽象化 react-router に内包された
domain-task SSR で非同期完了を待つ 廃止(JavaScript Services 由来)

移行メモ(原文の因果関係): 「redux-thunk の導入により
isomorphic-fetch が削除された」と読めるが、
fetch 自体は依然として必要である
(thunk の中で API を呼ぶため)。

【実際に起きたこと】
   ・fetch の呼び出し場所が、コンポーネント → thunk に移った
   ・SSR 対応のため、isomorphic-fetch ではなく
     【domain-task】が非同期の完了追跡を担うようになった
     (domain-task は内部で fetch をラップする)

json-loader の削除は別の理由で、
webpack 2 以降、JSON のインポートが標準サポートされたためである。

仕様

  • Redux 化
    setState を connect 関数に変更
    (props を使用して state と function を渡す)。

    • Counter.tsx
    • FetchData.tsx
  • BrowserRouter を使用したページング処理の追加

    • routes.tsx(URL にページ番号を組込む)
    • FetchData.tsx
  • サーバーサイド・レンダリング

    • index.html
    • boot-server.tsx

補足(connect と、現在の書き方): 原文の「setState を
connect 関数に変更」という要約は、Redux 化の本質を突いている

// 当時:connect で state と dispatch を props に注入する
class Counter extends React.Component<CounterProps> {
    render() {
        return <button onClick={() => this.props.increment()}>
                 Count: {this.props.count}
               </button>;
    }
}
export default connect(
    (state: ApplicationState) => state.counter,        // mapStateToProps
    CounterStore.actionCreators                        // mapDispatchToProps
)(Counter);
【connect が意味していたこと】
   ・コンポーネントは【Store を知らない】(props しか見ない)
      → 【テストしやすい】(props を渡すだけ)★
      → [依存性反転原則] と同じ発想
   ・connect が「繋ぐ」役をする

現在は Hooks を使うconnect も動くが、非推奨寄り)。

// 現在:react-redux の Hooks
export function Counter() {
    const count = useSelector((s: RootState) => s.counter.value);
    const dispatch = useDispatch();
    return <button onClick={() => dispatch(increment())}>Count: {count}</button>;
}

さらに現在は Redux Toolkit(RTK)が公式推奨である。

// Redux Toolkit:Action Type / Action Creator / Reducer が一度に作れる
const counterSlice = createSlice({
    name: 'counter',
    initialState: { value: 0 },
    reducers: {
        increment: state => { state.value += 1 },   // ← 直接書き換えて良い ★
    },
});
export const { increment } = counterSlice.actions;
【state.value += 1 と書けるのはなぜか】
   RTK は内部で【Immer】を使っている
     → 「書き換えたように見える」コードから、
       自動的に【新しいオブジェクト】を作る
     → 純粋関数の原則は守られている
     → スプレッド構文({...state, value: state.value + 1})の
       煩雑さから解放される

補足(そもそも Redux は今も必要か): これが現在の実務での
最初の問いになる。

【Redux が生まれた頃(2015)の状況】
   ・React に状態管理の標準がなかった
   ・Context API が未成熟
   ・データ取得のためのライブラリもなかった
     → 【全部 Redux でやる】ことになった
        → API のレスポンスまで Store に入れる
        → ボイラープレートが膨大に ★

現在は、状態を種類で分けて考える

状態の種類 現在の推奨
サーバの状態 API から取ったデータ TanStack Query / SWR
URL の状態 検索条件、ページ番号 URL のクエリ文字列(react-router)
フォームの状態 入力中の値 React Hook Form
UI の局所状態 開閉、タブ選択 useState
アプリ全体の状態 ログイン ユーザー、テーマ Context または Zustand / Jotai
複雑な業務状態 大規模な編集画面、履歴 Redux Toolkit(今も有効)
【原文のテンプレートの WeatherForecasts】
   → これは【サーバの状態】である
   → 現在なら Redux ではなく TanStack Query を使う
   → Store に入れる必要はない

Redux が今も適する場面:

・状態遷移が複雑で、【履歴・取り消しが要る】
・多くの画面が【同じ状態を共有する】
・状態の変化を【監査・記録したい】(Action がログになる)
・大規模チームで【状態変更の経路を強制したい】★

「Redux を使わない」ことは、現在では正しい選択であり得る——
という点が、当時との最大の違いである。

参考

開発基盤部会 Wiki

JavaScript > React / Redux

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

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

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

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