MS_ASPNETCoreReactReduxTemplate - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki
- æ»ãïŒ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ã远å ãããŠããã
è£è¶³ïŒ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 ã䜿ããªããããšã¯ãçŸåšã§ã¯æ£ããéžæã§ããåŸãââ
ãšããç¹ããåœæãšã®æå€§ã®éãã§ããã
React ã®ã»ã«ã³ãã»ã¹ããã
- 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