-
Notifications
You must be signed in to change notification settings - Fork 0
MS_ASPNETCoreReactReduxTemplate
- 戻る(ASP.NET Core SPAテンプレート)
- ASP.NET Core Angularテンプレート
- ASP.NET Core React.jsテンプレート
- ASP.NET Core React+Reduxテンプレート
※ 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
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-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.tsxとboot-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 に埋め込み、クライアントで復元する)。
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 を使わない」ことは、現在では正しい選択であり得る——
という点が、当時との最大の違いである。
- JavaScript(
DNET_JavaScript.md)- React(
DNET_React.md) - Redux(
DNET_Redux.md)
- React(
React のセカンド・ステップ(DNET_ReactSecondStep.md)
- Visual Studio での React と ASP.NET Core
https://learn.microsoft.com/ja-jp/visualstudio/javascript/tutorial-asp-net-core-with-react - ASP.NET Core での SPA の開発
https://learn.microsoft.com/ja-jp/aspnet/core/client-side/spa/intro
Tags: 移行, .NET開発, .NET Core, ASP.NET, ASP.NET Web API, ASP.NET SPA, JavaScript
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。