連載 第7回目です。
今回はとうとう ASP.NetCore に話を持っていくための序章としていこうと思います。今回も、.NetCoreのReact Redux のサンプルを使っていきます。
.Net Core の React Redux のサンプルの導入方法については第2回の記事をご参照ください。
今回はこのページの動作について見ていきます。
まずはコンポーネントごとに役割を確認
Reactコンポーネントの部分から見ていきましょう。 FetchData.tsx がこれに当たりますので、その中身を見ていきます。
publicrender(){return(<React.Fragment><h1id="tabelLabel">Weather forecast</h1><p>This component demonstrates fetching data from the server and working with URL parameters.</p>{this.renderForecastsTable()}{this.renderPagination()}</React.Fragment>);}
これによると、FetchDataページのコンポーネントはタイトルの "Weather forecast" と、そのキャプションがあって、その下に ForecastsTable と Pagination がある、という配置ですね。ForcastsTable から順に見ていきましょう。
renderForcastsTable()は以下のように実装されています。
privaterenderForecastsTable(){return(<tableclassName='table table-striped'aria-labelledby="tabelLabel"><thead><tr><th>Date</th><th>Temp. (C)</th><th>Temp. (F)</th><th>Summary</th></tr></thead><tbody>{this.props.forecasts.map((forecast:WeatherForecastsStore.WeatherForecast)=><trkey={forecast.date}><td>{forecast.date}</td><td>{forecast.temperatureC}</td><td>{forecast.temperatureF}</td><td>{forecast.summary}</td></tr>)}</tbody></table>);}
これは TodoApp を作っていれば簡単ですね。テーブルが実装されていて、ヘッダーとコンテンツに分かれています。コンテンツはテーブルの行が props のオブジェクト毎に用意される感じです。 this.props.forecasts
は、connect() メソッドで Redux の Store から受け取った state.weatherForcasts の中の1つです。
exportinterfaceWeatherForecastsState{isLoading:boolean;startDateIndex?:number;forecasts:WeatherForecast[];}exportinterfaceWeatherForecast{date:string;temperatureC:number;temperatureF:number;summary:string;}
ページの動きを見る限り、この this.props.forecasts
は 5つのエントリがあるようですが、まだここではわからないですね。別のどこかで実装されていることを期待して次に進みます。
次はPaginationの方を見ていきましょう。renderPaginationは次のように実装されています。
privaterenderPagination(){constprevStartDateIndex=(this.props.startDateIndex||0)-5;constnextStartDateIndex=(this.props.startDateIndex||0)+5;return(<divclassName="d-flex justify-content-between"><LinkclassName='btn btn-outline-secondary btn-sm'to={`/fetch-data/${prevStartDateIndex}`}>Previous</Link>{this.props.isLoading&&<span>Loading...</span>}<LinkclassName='btn btn-outline-secondary btn-sm'to={`/fetch-data/${nextStartDateIndex}`}>Next</Link></div>);}
なにやら気になる数字、 "5" がありますね。ここが先程の props にまでまわりまわって影響を与えていそうですね。
renderPagination()の中では、Link が使われています。これは React-Router-DOMの便利な人ですが、要はルーティングなので、この場合、URLの末尾につける数字を変えながら、再レンダリングをかけるイメージですかね。
to={`/fetch-data/${prevStartDateIndex}`}
この部分ですね。
prevStartDateIndex も nextStartDateIndex も、 this.props.startDateIndex
に 5 を足し引きした値なので、いずれにしてもこの startDateIndex がどういうふうに変わっていくのかを見る必要がありそうです。
はい、このページはこれでおしまいに見えますね。Reactコンポーネントだけで見ればこれでおしまいです。これではページのルーティングが実装されているだけで、状態の変更が行われるイメージが掴めないと思います。これを理解するには、Reactのライフサイクルについて触れる必要があります。
React のライフサイクルって何?
React にはライフサイクルと呼ばれる考え方が実装されています。
以下の記事がとてもわかり易かったです。
React(v16.4) コンポーネントライフサイクルメソッドまとめ
また、公式にもわかりやすい説明があります。
React.Componenet - component lifecycle
lifecycle-diagram
要約すると
- コンポーネントがはじめてレンダリングされるときから、コンポーネントが終了するときまでの過程をライフサイクルという
- ライフサイクル中に呼び出されるタイミングが決まったメソッドが予め用意されている
- render()は必須メソッドなので常にある
- 他にもいろいろあるけどオプショナルなので随時調べればいい
FetchData で使用されているメソッドは
- componentDidUpdate
- 更新が完了するたびに毎回
- componentDidMount
- マウントが完了した後1回だけ
の2つです。
fetchData では次のように使用されています。
publiccomponentDidMount(){this.ensureDataFetched();}publiccomponentDidUpdate(){this.ensureDataFetched();}
React Router からの Props
さて、componentDidMount も、 componentDidUpdate も同じように次の関数を呼び出していますね。
privateensureDataFetched(){conststartDateIndex=parseInt(this.props.match.params.startDateIndex,10)||0;this.props.requestWeatherForecasts(startDateIndex);}
それぞれどういうことをしているのか見ていきましょう。
conststartDateIndex=parseInt(this.props.match.params.startDateIndex,10)||0;
まずこちらは、 this.props.match.params.startDateIndex
が難解です。これを正確に理解するには React-Router のことを知る必要がありますが、ここでは結論だけ確認していきましょう。
まず、ここで見ている props
は React-Redux の connect() で渡された props ではありません。(正確には、 props は共通でも、見たいものは違うということ。)
ここで見たい props は React-Router が渡してくれる props です。 App.tsx を見てみてください。
exportdefault()=>(<Layout><Routeexactpath='/'component={Home}/><Routepath='/counter'component={Counter}/><Routepath='/fetch-data/:startDateIndex?'component={FetchData}/><Routepath='/todo'component={TodoApp}/></Layout>);
ここで Route タグでページを切り替えていたと思います。this.props.match
はこの Route タグから受け取る、 URL などの情報を指します。
match の中身はこんなふうになっています。
{
/* 1度 Next をクリックしたページの場合 (startDateIndex = 5 の場合) */
path: "/fetch-data/:startDateIndex?",
url: "/fetch-data/5",
isExact: true,
params: {
startDateIndex: "5"
}
}
つまり、this.props.match.params.startDateIndex
は上の例だと 5
ということになります。
一方で、読み込んだ直後のページの場合は startDateIndex はセットされていないため、 最後に ~ || 0;
という実装が加えられています。
これにより、 this.props.requestWeatherForecasts(startDateIndex);
は、 props により渡された requestWeatherForecasts() という関数に、 URL パラメーターを引き渡しているということになります。
では、次からは requestWeatherForecasts() を見ていきましょう。
React-Router についてもっと詳しく知りたい方はこの記事がとても良くまとまっていましたのでおすすめです。
関数が関数を返すから関数を渡して... 高階層な関数の実装について
requestWeatherForecasts() は React-Redux の connect() によって渡された関数です。 WeatherForecastsStore の actionCreators に実装されています。
exportconstactionCreators={requestWeatherForecasts:(startDateIndex:number):AppThunkAction<KnownAction>=>(dispatch,getState)=>{// Only load data if it's something we don't already have (and are not already loading)constappState=getState();if(appState&&appState.weatherForecasts&&startDateIndex!==appState.weatherForecasts.startDateIndex){fetch(`weatherforecast`).then(response=>response.json()asPromise<WeatherForecast[]>).then(data=>{dispatch({type:'RECEIVE_WEATHER_FORECASTS',startDateIndex:startDateIndex,forecasts:data});});dispatch({type:'REQUEST_WEATHER_FORECASTS',startDateIndex:startDateIndex});}}};
ここの解読は javascript (もしくは関数型プログラミング) に慣れていないと ??? となると思います。ひとつづつ見ていきます。
exportconstactionCreators={...}
これはもういいですね。 actionCreators として以下を定義して外に公開しますよ、という意味。
reqestWeatherForecasts:(startDateIndex:number):AppThunkAction<KnownAction>=>(dispatch,getState)=>{....}
reqestWeatherForecasts という名前で以下の関数を定義しますよ、というものなのですが・・・少し難しいですね。
ここが一番の難所です。関数を階層的に使っているところです。これは javascript の最近の書き方ですので、 Babel などをつかってトランスパイルしてやると、理解しやすいかもしれません。
まず、一番外側の関数はこういうふうになっています。
(startDateIndex:number):AppThunkAction<KnownAction>=>return(/* いろいろ */)
これはつまり、
- 引数が number 型の startDateIndex な関数
- 戻り値が AppThunkAction型 (ジェネリクスに KnownAction を渡す)な関数
- 戻ってくるものの詳細は あとで
ということです。 AppThunkAction型が気になりますので、 index.ts を見てみましょう。
exportinterfaceAppThunkAction<TAction>{(dispatch:(action:TAction)=>void,getState:()=>ApplicationState):void;}
- 引数が2つあり、 dispatch と getState という名前になっている (Redux の dispatch, getState とはここでは一旦別物) 関数で、戻り値はなし (一番うしろの void)
- dispatch は TAction (ジェネリクス) 型の引数 action をもち、何かを実行しておしまいな関数 (void なので何も返さない)
- getState は 引数はなく、戻り値は ApplicationState 型のなにかを返す関数
というふうに定義されています。
ここまでのことをまとめると、以下のようになります。
reqestWeatherForecastsは、
- 引数に number 型の startDateIndex を持つ関数であり、 AppThunkAction 型の戻り値を返す
- AppThunkAction は、引数に 関数の dispatch と getState をもつ関数であり、何も返さない
- dispatch は引数に KnownAction 型の action を持つ関数であり、何も返さない
- getState は引数を持たず、 ApplicationState 型の戻り値を持つ関数である
- AppThunkAction は、引数に 関数の dispatch と getState をもつ関数であり、何も返さない
ざっくり型だけで書くと、
f(number){/* いろいろな処理 */return(d(),g());}d(KnownAction){/* いろいろな処理 */returnvoid;}g(){/* いろいろな処理 */returnApplicationState;}
さて、続きを見ていきますと、
...=>(dispatch,getState)=>{....}
更に関数が続いていますね。後半は単純で、
- 引数に dispatch と getState をとり、なにか処理をするが何も返さない関数
- この dispatch と getState は Redux のそれである
というふうになります。
これと先の解釈をまとめれば、
f(number)(dispatch,getState)=>{...}
NOTE: ここでの dispatch と getState も Redux のそれである
という二段階の関数実装になっていることがわかりました。
こんな面倒な実装になっているのは、
- dispatchとgetStateはReactのコンポーネントに見せたい引数ではないから
- インターフェースとして切り出すことで実装を強制したいから
みたいな理由があると思います。(私見です)
説明が長くなってしまいましたが、大切なポイントなので仕方ないかなぁと思っています。
ASP.NetCore 側へ…
先の requestWeatherForecasts では、以下の実装が関数の本体でした。
constappState=getState();if(appState&&appState.weatherForecasts&&startDateIndex!==appState.weatherForecasts.startDateIndex){fetch(`weatherforecast`).then(response=>response.json()asPromise<WeatherForecast[]>).then(data=>{dispatch({type:'RECEIVE_WEATHER_FORECASTS',startDateIndex:startDateIndex,forecasts:data});});dispatch({type:'REQUEST_WEATHER_FORECASTS',startDateIndex:startDateIndex});}
まず、 appState に現在の state を取得します。
- appState があり、
- appState には weatherForecasts があり、
- startDateIndex が変わっている場合 (引数 != state)
上記の場合、if 文の中に入りますね。 JavaScript における true の扱いについては、いろいろなところに記事がありますし、入門書にも記載があることがおおいので、ここでは割愛します。
fetch('weatherforecast').then(/* ... */).then(/* ... */)
ここで使われている、 fetch
ですが、 実はこれが ASP.NetCore の WebAPI を叩いています。
ということで、今回はここまでで、次回はASP.NetCoreのWebAPIのことについて見ていこうと思います。