Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9529

C#お兄さんのReactやってみよう - 第5回: Todo App with Redux -

$
0
0

note_titleImage.png

連載第5回目です。
今回は前回とりあげた Counter ページを参考に、 Todo アプリ(簡易版)のページを作ってみようというものです。こちらも Redux で実装していきます。

まずは Todo アプリの外側を作ろう

Todo アプリの画面を追加しましょう。
追加する場所ですが、 Counter アプリ同様にページナビゲーションメニューに1つ追加する感じでやっていこうと思います。↓ここに1つ追加する感じですね。

ナビゲーションへのメニューの追加

なにはともあれナビゲーションにメニューを1つ追加してみましょう。
ナビゲーションのコンポーネントは components/NavMenu.tsx にあります。

NavMenu.tsx
publicrender(){return(<header><NavbarclassName="navbar-expand-sm navbar-toggleable-sm border-bottom box-shadow mb-3"light><Container><NavbarBrandtag={Link}to="/">demo</NavbarBrand><NavbarToggleronClick={this.toggle}className="mr-2"/><CollapseclassName="d-sm-inline-flex flex-sm-row-reverse"isOpen={this.state.isOpen}navbar><ulclassName="navbar-nav flex-grow"><NavItem><NavLinktag={Link}className="text-dark"to="/">Home</NavLink></NavItem><NavItem><NavLinktag={Link}className="text-dark"to="/counter">Counter</NavLink></NavItem><NavItem><NavLinktag={Link}className="text-dark"to="/fetch-data">Fetch data</NavLink></NavItem></ul></Collapse></Container></Navbar></header>);}

これを見る限り、 NavItem を追加すれば良さそうですね!
Todoのページは今はないので、ひとまずは counter のページを代用しましょう。こんな感じのコードを、 Fetch data の次に追加してみようと思います。

<NavItem><NavLinktag={Link}className="text-dark"to="/counter">Todo App</NavLink></NavItem>

無事レンダリングされていい感じのメニューができましたね。

React Router の概要

では、つぎに Todo App のためのページを作っていきたいですね。各ページは App.tsx に登録してあります。

App.tsx
exportdefault()=>(<Layout><Routeexactpath='/'component={Home}/><Routepath='/counter'component={Counter}/><Routepath='/fetch-data/:startDateIndex?'component={FetchData}/></Layout>);

ここについてはこれまでは触れてきませんでした。見た感じだと、 Layout タグの中に 3 つのコンポーネントがあるので、それぞれレンダリングされて表示されるのかなぁとも見えますが、実は違います。

Route タグは、 React-Router がもつコンポーネントです。これは、 path にマッチするものだけ、コンポーネントをレンダリング対象とするものです。

では、パスだけ変えてみましょう。
先程実装した NavLink の to の設定を "/todo" に変更してみましょう。

<NavItem><NavLinktag={Link}className="text-dark"to="/todo">Todo App</NavLink></NavItem>

この状態でテストしても、空っぽのページが表示されるだけだと思います。
では、先程の Route で /todo のパスにマッチしたときも Counter ページを表示するようにしてみましょう。

App.tsx
exportdefault()=>(<Layout><Routeexactpath='/'component={Home}/><Routepath='/counter'component={Counter}/><Routepath='/fetch-data/:startDateIndex?'component={FetchData}/><Routepath='/todo'component={Counter}/></Layout>);

こうすると、無事に Todo App のナビゲーションをクリックしても、 Counter 画面が表示されるようになりました。このとき、 URL は /todo になっていることが確認できますね。

Todo アプリコンポーネントを追加しよう

では、本題の Todo アプリコンポーネントを追加してみましょう。
Components フォルダ下に TodoApp.tsx というコンポーネントを追加してみましょう。
とりあえずこんな感じにしてみました。全然まだ Todo アプリでもなんでも無いですが、画面を用意するだけ、やっちゃいましょう。

components/TodoApp.tsx
import*asReactfrom'react';import{connect}from'react-redux';constTodoApp=()=>(<div><h1>Todoアプリだよ</h1><ul><li>やることその1</li><li>やることその2</li><li>やることその3</li></ul></div>);exportdefaultconnect()(TodoApp);

TodoApp リンクをクリックしたときにこのコンポーネントをレンダリングした画面が表示されるようにしたいので、次に、App.tsx の Route を書き換えていきます。

まず、使用したいコンポーネントをインポートして・・・

importTodoAppfrom'./components/TodoApp';

Route の使用するコンポーネントに指定します。

<Routepath='/todo'component={TodoApp}/>

では、結果を確認しましょう。

いい感じですね。
では、続いて Todo アプリの本体を作っていきましょう。ここからが大変ですけど、一番楽しいところですね。

Todo アプリの本体を Counter を参考に作ろう

ではさっそく Todo アプリらしくしていこうと思います。
全体のイメージとしては、

  • タスク名の入力欄がある
  • ボタンを押すと入力欄のタスクが追加される
  • タスクはリスト形式で表示
  • タスクには削除(完了)ボタンがついている

っていう感じです。どの情報を Redux Store に持っていくか、考えながら作っていきます。
まず、そういう難しいところ以外からやっていきましょう。

完成イメージのお絵かきをしよう

なんともくだらないタイトルに見えますが、これが一番重要で難しいんじゃないかなぁと思っています。

どんな見た目がいいかなぁと考えていたときに、「そうだ、Fetch data のページがいい感じだったな」ということに気づきました。 Fetch data のページはこんな感じになっています。

image.png

いいですね。いい感じにおしゃれですね。これなら完成したぞっていう満足感がありますね。

(´・ω・)(・ω・`)ネー

ではこのイメージでちょちょっとお絵描きするとこんな感じではないでしょうか?

image.png

めちゃくちゃ雑で恐縮ですが、こんな感じでテーブルっぽい見た目にしてタスクを並べていったらいいんじゃないかなぁと考えています。

ではさっそく、タスクをいれるテーブルの下地から取り掛かっていきましょう。

テーブルの下地をつくる

テーブルの下地は Fetch data のページを参考にしましょう。 Fetch data は ASP.NetCore とも関係があるページなので、深入りせず、ただ見た目だけを借りるんだと、心に誓って、いざオープンです。

...

FetchData.tsx はまぁまぁボリューム感がありますね。ここにコピペするのはやめておきます。
なにはともあれ、今回使いたいテーブルの部分はきっとここですね。

FetchData.tsx
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>);}

とりあえずテーブルにいい感じの ClassName を指定しておけば、 Bootstrap がきれいにしてくれそうですね。
さっそく TodoApp 側にもこのテーブルを配置しましょう。中身はとりあえず適当に書いておきましょう。

TodoApp.tsx
import*asReactfrom'react';import{connect}from'react-redux';constTodoApp=()=>(<div><h1> Todo App </h1><tableclassName='table table-striped'aria-labelledby="tabelLabel"><thead><tr><th>TASK</th><th></th>{/*ボタンを置くだけなので、スルー*/}</tr></thead><tbody><tr><td>すごいアプリをつくる</td><td></td></tr><tr><td>Herokuにデプロイする</td><td></td></tr></tbody></table></div>);exportdefaultconnect()(TodoApp);

やりましたね〜
テーブルができましたね

image.png

では次に、ここにボタンを配置しましょう。ボタンは Counter.tsx からもらってきましょう。

Counter.tsx
<buttontype="button"className="btn btn-primary btn-lg"onClick={()=>{this.props.increment();}}>Increment</button>

increment()関係の処理はいらないので、消してしまいましょう。うまくボタンは配置できそうですが、このままだとボタンがなんか青いですね。ちょっと目立ちすぎですね。あとなんかデカイですね。

image.png

ボタンをもう少し小さくして、色を落ち着いた感じにしたいです。Bootstrap で用意されているものから選んでみましょう。公式のページも見てみたのですが、こちらのブログのほうが見たい情報だけまとまっていたので、いい感じでした。

Bootstrap4に用意されているクラス【ボタン編】

まず、 btn-primaryですが、これが青い色の元になっているみたいですね。普通に白色がいいので、 btn-lightに変えちゃいます。

次に、 btn-lgですが、これはボタンの大きさを指定するものみたいですね。テーブルの中にいれるので普通より少し小さいほうがいいなぁと思ったので、 btn-smを使うことにしました。

image.png

今度は枠が無いからわかりにくいですねぇ・・・。あとちょっとですね。調べてみると、アウトラインボタンというのがあるみたいですね。こっちのほうがスッキリした見た目になりそうです。
btn-lightbtn-outline-darkに変えてみます。

image.png

じゃーん!! すっごくいい感じになりました!! デザインセンスなんていらないのかもしれないです(そんなことない)

テーブルの登録内容を可変にする

最難関に差し掛かってまいりました。 Redux に少しづつ手を出していかないといけないところにやって参りました。
まずはこれまでただの const で作ってきた TodoApp.tsx を React.Component のクラスへと変更していく必要がありますね。

TodoApp.tsx
classTodoAppextendsReact.PureComponent{publicrender(){return(<React.Fragment>{/* ここにさっきまでのTodoアプリ本体 */}</React.Fragment>);}};

こんな感じにすればとりあえずは見た目は変わらず、クラスへと変更することができます。 もともと全体を div タグで括っていたのですが、不要な div だったので、 React.Fragment へ変更しました。

では、 Todo のリストの中身を Redux で扱うようにしていきます。
このイメージは Counter を調べたときの counter state を props まで持ってきた部分が参考になります。

まずは connect に渡す state を作っていきましょう。
このアプリケーションでは、すべての state を ApplicationState に一旦集めるようになっていましたね。TodoApp もこのお作法に則っていきましょう。

まず、必要そうなものをインポートします。

TodoApp.tsx
import{RouteComponentProps}from'react-router';import{ApplicationState}from'../store';

つぎに、 ApplicationState を見てみましょう。

index.ts
exportinterfaceApplicationState{counter:Counter.CounterState|undefined;weatherForecasts:WeatherForecasts.WeatherForecastsState|undefined;}

ここに追加したいですね。追加するためには TodoApp もそれ専用の定義が必要そうですね。では早速作っていきましょう。

store フォルダの下に TodoApp.ts ファイルを新規作成します。中身は Counter.ts を参考にしながら作り込んでいきましょう。

TodoApp.ts
exportinterfaceTodoAppState{Tasks:Task[];}exportinterfaceTask{TaskName:string;TaskStatus:boolean;}

とりあえずアクションはあとで実装するので、 State だけ定義してみました。
Todo アプリなので、1行をひとつの Task と定義すれば、そのリストになりますね。
Task の中身はタスク名とそのステータス(今回は Done を押したとき、削除するのではなく、横線を引きたいから、完了済みのものも残します。世間のアプリもだいたいそんな感じですよね。)

この定義により、 ApplicationState を更新することができます。

import*asWeatherForecastsfrom'./WeatherForecasts';import*asCounterfrom'./Counter';import*asTodoAppfrom'./TodoApp';// The top-level state objectexportinterfaceApplicationState{counter:Counter.CounterState|undefined;weatherForecasts:WeatherForecasts.WeatherForecastsState|undefined;todoApp:TodoApp.TodoAppState|undefined;}// ~~~~exportdefaultconnect((state:ApplicationState)=>state.todoApp)(TodoApp);

無事、todoApp を追加することができました。 connect にも登録できましたね。
ついでに、 PureComponent にも Props を登録しておきましょう。

TodoApp.tsx
typeTodoAppProps=TodoAppStore.TodoAppState&//typeof TodoAppStore.actionCreators &   // 未実装RouteComponentProps<{}>;classTodoAppextendsReact.PureComponent<TodoAppProps>{publicrender(){return(// ~~~

さて、ここまでやれば、 props が使えるようになるので、テーブルの中身を props から取得するように変更していきましょう。

TodoApp.tsx
import*asReactfrom'react';import{connect}from'react-redux';import{RouteComponentProps}from'react-router';import{ApplicationState}from'../store';import*asTodoAppStorefrom'../store/TodoApp';typeTodoAppProps=TodoAppStore.TodoAppState&//typeof TodoAppStore.actionCreators &   // 未実装RouteComponentProps<{}>;classTodoAppextendsReact.PureComponent<TodoAppProps>{publicrender(){return(<React.Fragment><h1>Todo App</h1><tableclassName='table table-striped'aria-labelledby="tabelLabel"><thead><tr><th>TASK</th><th></th>{/*ボタンを置くだけなので、スルー*/}</tr></thead><tbody>{(typeof(this.props.Tasks)==undefined)?this.props.Tasks.map((task:TodoAppStore.Task)=><tr><td>test</td><td><buttontype="button"className="btn btn-outline-dark btn-sm">
                                            Done
                                            </button></td></tr>):<tr><td>No Task</td><td></td></tr>}</tbody></table></React.Fragment>);}};exportdefaultconnect((state:ApplicationState)=>state.todoApp)(TodoApp);

はい、なんか勝手にややこしい実装をしてすいません。たとえ props が届くようになっても、 Tasks は空っぽですよね。誰も定義していないので。そのため、props の中身がないときは No Taskと表示するようにしてみました。

{(typeof(this.props.Tasks)==undefined)?/*YESのとき*/:/*NOのとき*/}

三項演算子で表示を切り替えてみました。ここまでくると、画面はこんな感じになりますね。

さて、いつまでも No Taskではダメなので、タスクの登録画面を作っていきましょう。

タスクの入力欄と登録ボタンを作る

テーブルの上のところに、タスクの登録機能をつけていきましょう。
Bootstrapのページで入力欄の作り方を調べてみました。

Bootstrap - Input group -

入力欄の隣にボタンが付いている例がありますね。すごく良さそうです。これを採用しましょう。

TodoApp.tsx
classTodoAppextendsReact.PureComponent<TodoAppProps>{publicrender(){return(<React.Fragment><h1>Todo App</h1><divclassName="input-group mb-2"><inputtype="text"className="form-control"/><divclassName="input-group-append"><buttontype="button"className="btn btn-outline-secondary">Add</button></div></div>

はい、サクッと追加してみました。注意点としては Bootstrap のページでは class が使われていましたが、 className にしてあります。 React では class が予約されているので、 Bootstrap では className を使う必要があります。参考: Stackoverflow

image.png

いい感じにハマりましたね。

次は入力した内容を取得して、 Redux に渡していくところですね。 さっきはスキップした、 Action の実装をやっていきましょう。

入力内容を取得して、Reduxのアクションとして登録する

Todoアプリとして動いてもらうためにはテキストボックスの入力を読み取り、登録する仕組みが必要です。まずはボタンをクリックしたときに状態が変わる仕組みを作っていきたいと思います。
この動きは Counter でも既に実装されていましたね。まずはほとんど同じように作っていけばいいと思います。

では、アクションの定義からやっていきましょう。
改めて Counter コンポーネントの実装を見ていきましょう。

Counter.tsx
classCounterextendsReact.PureComponent<CounterProps>{publicrender(){return(<React.Fragment><h1>Counter</h1><p>This is a simple example of a React component.</p><paria-live="polite">Current count: <strong>{this.props.count}</strong></p><buttontype="button"className="btn btn-primary btn-lg"onClick={()=>{this.props.increment();}}>
                    Increment
                </button></React.Fragment>);}};

この中で、ボタンのクリックに対する動作を規定しているのは

onClick={()=>{this.props.increment();}}

の部分ですね。これはすなわち、 props が関数を持っていることになります。 props は渡されない限りこういうものは持たないので、 Redux から受け取っているはずです。受け取りは connect() で実装されていましたね。
このあたりに注目しながら、 TodoApp コンポーネント側の実装に着手してみましょう。

Counter.tsx
exportdefaultconnect((state:ApplicationState)=>state.counter,CounterStore.actionCreators)(Counter);

Counter のこの部分の実装を見ると、 actionCreators が登録されている事がわかります。これが increment() を提供しているはずです。では、同じように TodoApp にも actionCreators をもたせていきましょう。

TodoApp.tsx
exportdefaultconnect((state:ApplicationState)=>state.todoApp,TodoAppStore.actionCreators)(TodoApp);

と、すると、actionCreatorsの定義がないよと怒られると思います。(怒ってくれる Visual Studio に感謝)
怒られたので、定義をしていきましょう。 TodoApp.ts に書き込んでいくことになります。

TodoApp.ts
exportinterfaceAddTaskAction{type:'ADD_TASK_ITEM'}exportconstactionCreators={addTask:()=>({type:'ADD_TASK_ITEM'}asAddTaskAction)};

こんな感じですかね。完全に思考停止して Counter を真似しました。
次に、この actionCreators に登録されている Action をつかって state の更新をする Reducer を実装してみましょう。

TodoApp.ts
exporttypeKnownAction=AddTaskAction;exportconstreducer:Reducer<TodoAppState>=(state:TodoAppState|undefined,incomingAction:Action):TodoAppState=>{if(state===undefined){return{Tasks:newArray()};}constaction=incomingActionasKnownAction;switch(action.type){case'ADD_TASK_ITEM':return{Tasks:[...state.Tasks,defaultTask()]};default:returnstate;}};

ついでに KnownAction を定義しておきました。あとで Action を増やす予定なので、そのときに使いましょう。
Reducer ですが、本来は入力フィールドの値を受け取るようにすべきですが、一旦後回しにしましょう。Redux の実装に集中した方が、他のことに気を取られずに進めることができるので、 開発の進め方としておすすめしています。(会社の後輩とかには)

さて、この実装の通りに動くなら、 addTask() が実行されるたびに state の Tasks にデフォルト値が登録されるということになります。サラッと実装していましたが、 defaultTask は次のように実装しています。

TodoApp.ts
constdefaultTask=():Task=>({ID:0,TaskName:'default',TaskStatus:false});

これまで、 Task インターフェイスには ID という定義はありませんでした。 Todo アプリを作るにあたって、登録の通し番号くらいはほしいなぁと思ってとりあえず追加しています。

さて、 Reducer に関してはもうひと仕事あります。 Reducer を createStore にセットすることです。これをやっておかないと、たとえ addTask() を呼んだとしても、 state の更新が行われず、再描画もされません。

index.ts
portconstreducers={counter:Counter.reducer,weatherForecasts:WeatherForecasts.reducer,todoApp:TodoApp.reducer};

reducers に追加しました。 reducers に追加しておけば、別のところで createStore にセットしてくれるようになっていますので、これでOKです。

最後に、登録情報がないときの仕組みを少し変えておきました。(実際に動かしてみたら、ちょっとブサイクだったからです)

TodoApp.tsx
<tbody>{(typeof(this.props.Tasks)!==undefined)?this.props.Tasks.map((task:TodoAppStore.Task)=><trkey={task.ID}><td>{task.TaskName}</td><td><buttontype="button"className="btn btn-outline-dark btn-sm">DONE</button></td></tr>):<tr><td> No Task</td><td></td></tr>
    }
</tbody>

ここまで実装すると、こんな感じに動くようになります。

todo.gif

ちょっと DONE ボタンの位置が気になりますが、まぁ良しとしましょう。

次に、入力フィールドの情報を取得し、 addTask() でタスクが増えるようにしてみましょう。これは入力フィールドの値を取得し、登録してあげればいいので、文字列をまるっと取得して addTask() に渡してあげるのがいいのかなぁとも思ったのですが、少しやり方を変えようと思います。

今回はそこまで実装しませんが、例えば入力された情報に対するチェック機能の実装を考えたとき、その実装はフロント側で実施すべきでしょうか? 私はなるべくそういうものはフロント側には書きたくない性分です。また、入力の情報は逐次変わっていきます。 Add ボタンを押したけど登録できない、エラーが表示される、みたいな 「後から分かる系エラーチェック」って正直ストレッサー以外の何物でもないですよね。入力情報を常に監視しながら、良くないのならどう良くないのかを教えてくれたり、ボタンの有効無効が連動したりするほうが、UXはいいと思います。

今回はそれを意識して、入力内容を逐一 State に送り込む仕組みにしましょう。そうしておけば、あとからいろいろな展開が可能になります。

では、入力の情報を逐一チェックして情報を送るようにしていきます。
逐一チェックするというところは、 input フィールドに対して onChange イベントを実行してあげれば大丈夫です。onChange の中で、 Action を作って実行し、 State を更新するようなイメージです。では、実装してみるとこんなふうになりました。

TodoApp.tsx
<divclassName="input-group mb-2"><inputtype="text"className="form-control"onChange={(ev)=>this.props.updateInput(ev.target.value)}/><divclassName="input-group-append"><buttontype="button"className="btn btn-outline-secondary"onClick={()=>{this.props.addTask()}}>
            Add
        </button></div></div>

ここで updateInput() という関数を使っています。これは入力内容の反映用に追加した新しい Action です。
TypeScript 側を見てみましょう。

TodoApp.ts
import{Action,Reducer}from'redux';exportinterfaceTodoAppState{Input:string,Tasks:Task[];}exportinterfaceTask{ID:number,TaskName:string;TaskStatus:boolean;}constdefaultTask=():Task=>({ID:0,TaskName:'default',TaskStatus:false});exportinterfaceAddTaskAction{type:'ADD_TASK_ITEM'}exportinterfaceUpdateInputAction{type:'UPDATE_INPUT',newTask:string}exporttypeKnownAction=AddTaskAction|UpdateInputAction;exportconstactionCreators={addTask:()=>({type:'ADD_TASK_ITEM'}asAddTaskAction),updateInput:(newTask:string)=>({type:'UPDATE_INPUT',newTask:newTask}asUpdateInputAction)};exportconstreducer:Reducer<TodoAppState>=(state:TodoAppState|undefined,incomingAction:Action):TodoAppState=>{if(state===undefined){return{Input:"",Tasks:newArray()};}constaction=incomingActionasKnownAction;switch(action.type){case'ADD_TASK_ITEM':constnewTask:string=state.Input;return{Input:"",Tasks:[...state.Tasks,{ID:state.Tasks.length,TaskName:newTask,TaskStatus:true}]};case"UPDATE_INPUT":return{Input:action.newTask,Tasks:state.Tasks}default:returnstate;}};

ここでは、 updateInput() というアクションを新たに定義し、引数に文字列を渡し、 Reducer がそれを State に反映するようにしました。

これで、 タスクの追加が実装できました。実際の動きを見てみましょう。

todoapp.gif

つぎは DONE ボタンの挙動とか・・・・

長くなってきたのでこの記事はここまでにします!
タスクの追加はできるので問題ないですね!

(´・ω・)(・ω・`)ネー

ただし、この実装でお気づきの方もいらっしゃるかもですが、画面をリフレッシュした時点で消えてしまいます。理由は簡単、サーバーサイドに持って行ってないからです。それはまた先のお話・・・。

次回にやりたいこと

DONE ボタンの挙動をかっちょよくするぞ
タスクの削除に対応するぞ
細かいところをブラッシュアップするぞ


Viewing all articles
Browse latest Browse all 9529

Trending Articles