React 入門 (6) ~ TODOアプリを作る(1) ~
今まで作ってきた環境でTODOアプリを作る。
まずは単純に、TODOの登録ができて、TODOを完了させるチェックボックスを付与するくらいの小さいものにする。
静的なリストを作成
少しずつ作って完成に近づけたほうがちょっとした修正もしやすくなるはず。ということで、まずは静的にリストを作成する。
まずはリスト用のコンポーネント src/components/NumberList.tsx
を作成する。
今回は 1~5までの数字を羅列するのみ。
import * as React from 'react'; export class NumberList extends React.Component { listItems = [1,2,3,4,5].map((number) => <li>{number}</li> ); render() { return ( <ul>{this.listItems}</ul> ) } }
次に、src/index.tsx
で NumberList
を呼び出す
import * as React from "react"; import * as ReactDOM from "react-dom"; import { NumberList } from "./components/NumberList"; ReactDOM.render( <NumberList />, document.getElementById("root") );
実行結果
これで、npm start
を実行すると以下のような画面が表示されるはずだ。
動的追加
+
ボタンを追加して、list アイテムを動的に追加できるようにする。
ただ、コンポーネント間で値を同期させるやり方がわからない。
以下のURLを参考に色々試すが、うまくいかない。
というのも、初めはTodo コンポーネントを親コンポーネントにして、ボタンコンポーネントとリストするコンポーネントをTodoコンポーネントの下にぶら下げる設計だったので、ボタンコンポーネントで更新したstateをリストするコンポーネント側に渡す必要があり、stateを参照渡しで渡せればいいのだが、それをやる方法がわからず、ボタンコンポーネントで更新したstateをいったんTodoコンポーネントに渡して、それをリスト側に反映する。といったようなことをしないといけないわけだが、それがどうもうまくいかない。
なので、もうTodoコンポーネントのしたに、ボタンとリストを一括で行う子コンポーネントのみにし、子コンポーネントの中ですべてを行うようにした。それが以下のコードになる。上記で作成した src/components/NumberList.tsx
に追記する形で作成。
import * as React from 'react'; import { TodoState } from "./TodoStateInterface"; export class NumberList extends React.Component<{}, TodoState> { text: string; textRef = React.createRef<HTMLInputElement>(); constructor(props: any) { super(props); this.state = { items: [] }; this.text = ""; } updateState = (state: TodoState) => { this.setState(state) } listItems = (state: TodoState) => { return state.items.map((text, i) => { if(text!="") { return <li key={i}>{text}</li> } return null; }); } on_click = () => { this.state.items.push(this.text); this.textRef.current!.value = ""; this.updateState(this.state); } on_change = (e: React.FormEvent<HTMLInputElement>) => { console.log(e.currentTarget.value); this.text = e.currentTarget.value; } render() { return ( <div> <div> <button onClick={this.on_click}>+</button> <input ref={this.textRef} type="text" onChange={this.on_change} /> </div> <ul>{this.listItems(this.state)}</ul> </div> ) } }
特記事項としては、2つ。key
と ref
について。listItems
を作成するときに <li>
の要素を返すわけだが、key
を指定しないとデバッグツールでワーニング(?)が表示される。その理由は以下の記事が詳しい。
React.jsの地味だけど重要なkeyについて - Qiita
次の ref は 子コンポーネントの参照を自コンポーネントに持つことができる技術で、以下の記事が詳しい。
React Refs with TypeScript – Martin Hochel – Medium
コード中でインポートしている TodoStateInterface
は Todo の状態を管理するためのインターフェースで以下のようになっている。
export interface TodoState { items: string[] }
実行結果
上記コードを動かすと以下のようになるだろう。
完了処理を追加
TODOを追加できるようになったので、次は完了処理を追加する。li
要素をクリックすると文字に取り消し線が入るだけの簡単なもの。
まず、TodoStateInterface.tsx
に完了フラグを追加する string[]
では要素ごとに保持できないので、Item
インターフェースを別途作って、 TodoState
ではその配列を保持する。
export interface Item { todo: string done: boolean } export interface TodoState { items: Item[] }
NumberList.tsx
も修正する。修正するのは以下の2つ
li
要素がクリックされたときのイベントの捕捉done
フラグによる要素レンダリング処理の分岐
上記2点を行ったのが以下のコード
import * as React from 'react'; import { TodoState, Item } from "./TodoStateInterface"; export class NumberList extends React.Component<{}, TodoState> { text: string; textRef = React.createRef<HTMLInputElement>(); constructor(props: any) { super(props); this.state = { items: [] }; this.text = ""; } updateState = (state: TodoState) => { this.setState(state) } on_click_li = (e: React.MouseEvent<HTMLLIElement>) => { var id:number; id = +(e.currentTarget.id) this.state.items[id].done = true this.updateState(this.state); } on_click = () => { this.state.items.push({ todo:this.text, done:false}); this.textRef.current!.value = ""; this.updateState(this.state); } on_change = (e: React.FormEvent<HTMLInputElement>) => { this.text = e.currentTarget.value; } drawItems = (item: Item) => { if(item.done) { return <s>{item.todo}</s> } return item.todo } listItems = (state: TodoState) => { return state.items.map((item, i) => { if(item.todo!="") { return <li id={String(i)} key={i} onClick={this.on_click_li}>{this.drawItems(item)}</li> } return null; }); } render() { return ( <div> <div> <button onClick={this.on_click}>+</button> <input ref={this.textRef} type="text" onChange={this.on_change} /> </div> <ul>{this.listItems(this.state)}</ul> </div> ) } }
ここに来るまでに、少々問題があった。一つ目の問題としては、li
をクリックしたときに飛ばされるイベント MouseEvent<HTMLLIElement>
からとれる情報の中にどのli
要素がクリックされたかの情報がそのままでは取れなかったことがある。key
属性の値が取れればいいのだが、どうもとれなさそうだった。色々調べた結果、最終的に id
属性を追加することにした。次に、id
属性を event オブジェクトからどうやって取得するかが問題となった。結果としては currentTarget
プロパティに id
属性があるので、それで取得した。
実行結果
上記を実行すると以下のような感じになるだろう。
まとめ
簡単なTodo アプリを作り始めた。追加と完了は作成できたので、次は削除処理を作成していきたい。また、値が揮発するので永続化もしなければならないだろうし、コンポーネントも1つだけしか使っていないのがあまり設計上よいとはいえない。Redux等を使えばそのあたりは解決できるのだろうか? 後は、実装をするだけしてテストを一切かけていないので、テストも書いていきたい。
今回の成果物
以下に今回の成果物は上げている。興味があれば見てみてほしい。
GitHub - bamchoh/react-study at 2019-01-27
おまけ (2018-01-28 追記)
各TODO要素に削除ボタンを追加したものを作成したので、一応の成果物としてここにリンクを張っておく。 特段記事にすることもないので、リンクだけ。