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.tsxNumberList を呼び出す

import * as React from "react";
import * as ReactDOM from "react-dom";

import { NumberList } from "./components/NumberList";

ReactDOM.render(
    <NumberList />,
    document.getElementById("root")
);

実行結果

これで、npm start を実行すると以下のような画面が表示されるはずだ。

f:id:bamch0h:20190127155109p:plain
NumberListの表示サンプル

動的追加

+ ボタンを追加して、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つ。keyref について。listItems を作成するときに <li> の要素を返すわけだが、keyを指定しないとデバッグツールでワーニング(?)が表示される。その理由は以下の記事が詳しい。

React.jsの地味だけど重要なkeyについて - Qiita

次の ref は 子コンポーネントの参照を自コンポーネントに持つことができる技術で、以下の記事が詳しい。

React Refs with TypeScript – Martin Hochel – Medium

コード中でインポートしている TodoStateInterface は Todo の状態を管理するためのインターフェースで以下のようになっている。

export interface TodoState {
  items: string[]
}

実行結果

上記コードを動かすと以下のようになるだろう。

f:id:bamch0h:20190127211112g:plain
Todo 動的追加

完了処理を追加

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 属性があるので、それで取得した。

実行結果

上記を実行すると以下のような感じになるだろう。

f:id:bamch0h:20190127230727g:plain
Todo 完了処理

まとめ

簡単なTodo アプリを作り始めた。追加と完了は作成できたので、次は削除処理を作成していきたい。また、値が揮発するので永続化もしなければならないだろうし、コンポーネントも1つだけしか使っていないのがあまり設計上よいとはいえない。Redux等を使えばそのあたりは解決できるのだろうか? 後は、実装をするだけしてテストを一切かけていないので、テストも書いていきたい。

今回の成果物

以下に今回の成果物は上げている。興味があれば見てみてほしい。

GitHub - bamchoh/react-study at 2019-01-27

おまけ (2018-01-28 追記)

各TODO要素に削除ボタンを追加したものを作成したので、一応の成果物としてここにリンクを張っておく。 特段記事にすることもないので、リンクだけ。

GitHub - bamchoh/react-study at 2019-01-28_add_del_button