Amsel's diary

主にプログラミングの勉強で得た知識を書き溜めておくブログです。

Reactを使ってTodoアプリを作ってみる_3日目

初めに

この記事はReact初心者が初めての自作アプリ作成を行う様子を書き残した日記です。

前回はFormコンポーネントを作成し、タスク登録機能を実装しました。 今回は登録されたタスクを表示する機能の実装に着手します。

タスク表示機能

登録したタスクを表示する機能はTodoListコンポーネントが担当します。

前回、Formコンポーネントのstateに登録されたタスクを保存するようにしました。
今回はその情報をpropsを通じてTodoListコンポーネント渡すようにします。

タスク追加機能の実装は以下となります。
TodoListコンポーネントの引数にtodosを追加し、タスク一覧の情報を渡してTodoListコンポーネント側に渡す流れになります。

src/App.js コンポーネント呼び出し部分

<React.Fragment>
        <h1>Todo App</h1>
        <Form value={this.state.value} handleChange={this.handleChange} handleClick={this.handleClick} />
        <TodoList/>
        <TodoList todos={this.state.todos} /> 
      </React.Fragment>

src/TodoList.js

render() {
     return (
         <ul>
            {this.props.todos.map(
                (todo) => <li>{todo}</li>
            )}
         </ul>
     );
}

タスク一覧の表示にはJavaScripのmapメソッドを使用しています。
mapメソッドは引数に処理を渡すと、配列のすべての要素にその処理を実行し、その結果から得られた新しい配列を生成します。

つまり以下の処理はtodosという配列から1つ要素を取りしてtodoに格納し、そのtodoに<li>タグを追加する、という処理を配列の1つ1つの要素に対して実行する処理となります。

this.props.todos.map((todo) => <li>{todo}</li>)

これでタスクがリスト化されて画面に表示されるようになりました。

f:id:Amsel1676:20200510183833g:plain

タスク削除機能

次は登録されたタスクを削除する機能を追加します。
タスクの隣にDeleteボタンを追加し、そのボタンが押されるとそれに紐づくタスクが削除される、という処理を実装します。

さて、その機能を実装したソースは以下の通りとなります。

src/TodoList.js

import React from 'react'

class TodoList extends React.Component {
    render() {
        return (
            <ul>
               {this.props.todos.map(
                   (todo, i) => <li key={i}><input type="checkbox" />{todo}<button onClick={() => this.props.handleDeleteClick(i)}>Delete</button>
                   </li>
               )}
            </ul>
        );
    }
}

export default TodoList;

src/App.js handleDeleteClickメソッド

 handleDeleteClick = (index) => {
    const newTodo = this.state.todos.slice()
    newTodo.splice(index,1)
    this.setState({todos: newTodo})
  }

src/App.js renderメソッド

render() {
    return (
      <React.Fragment>
        <h1>Todo App</h1>
        <Form value={this.state.value} handleChange={this.handleChange} handleClick={this.handleClick} />
        <TodoList todos={this.state.todos} /> 
        <Form value={this.state.value} handleChange={this.handleChange} handleAddClick={this.handleAddClick} />
        <TodoList todos={this.state.todos} handleDeleteClick={this.handleDeleteClick} /> 
      </React.Fragment>
    );
  }

handleDeleteClickメソッド

handleDeleteClickメソッドはDeleteボタンがクリックされた時の動作を定義したメソッドです。
これをpropsを通じてTodoListコンポーネントに渡すことで、Appコンポーネントで定義したメソッドをTodoListコンポーネント側で使うことができます。

渡されたhandleDeleteClickAppメソッドはAppコンポーネントのstateを参照するので、Appコンポーネントで管理しているstateの情報から対象の情報を削除する、といった処理が実現可能になります。

Key

また、クリックされた要素を特定するため、ReactのKeyという機能を使用しています。

Keyはどの要素が変更、追加、もしくは削除されたのかをReactが識別できるようにする機能です。
要素にkeyというキーワードを与えることで、その要素を一意に特定できるような情報を付与することができます。

this.props.todos.map((todo, i) => <li key={i}><input type="checkbox" />{todo}<button onClick={() => this.props.handleDeleteClick(i)}>Delete</button></li>)

今回はDeleteボタンが押されたのがどの要素なのか特定できるようにする必要があるため、mapによって出力された要素に降順の番号を付与しています。

Deleteボタンが押されたとき、handleDeleteClickメソッドにその番号を渡すことで配列内のどの要素を消すのか指定するような仕組みとなっています。

おわりに

今回はこれまでより複雑なロジックになってきました。 登録機能、削除機能を実装するために活用した以下のメソッド、機能をキチンと押さえておこうと思います。

おまけ:ハマりポイント

実は削除機能を実装するにあたって長い間ハマっていたところがあるので、自戒の念を込めて書き残しておきます。

イベントハンドラに引数付きのメソッドを追加する

handleDeleteClickメソッドをonClickイベントに登録する時、以下のように書くと予期せぬ挙動を取りました。

src/TodoList.js

<button onClick={this.props.handleDeleteClick(i)}>Delete</button>

タスクが追加されたタイミングでhandleDeleteClickメソッドが呼ばれ、登録されたタスクが即座に消える、というバグが発生したのです。

このバグの原因ですが、this.props.handleDeleteClick(i)という箇所がメソッドの登録ではなく、メソッドの実行としてReactに認識されているためです。

handleDeleteClickメソッドは引数を必要とするメソッドなので、handleDeleteClick(i)と書いたのですが、ここのカッコによりメソッドの実行であると判断されてしまったようです。
そのためタスクが描画された時にhandleDeleteClickメソッドが実行されるというバグが発生してしまいました。

引数付きのメソッドをイベントハンドラに登録したいのであれば、メソッドの登録だと明示的にしてあげましょう。

その方法の1つがアロー関数による無名関数の定義です。

<button onClick={() => this.props.handleDeleteClick(i)}>Delete</button>

これによりthis.props.handleDeleteClick(i)というメソッドが無名の関数定義として処理され、正しくイベントハンドラに登録されるようになります。

Reactを使ってTodoアプリを作ってみる_2日目

初めに

この記事はReact初心者が初めての自作アプリ作成を行う様子を書き残した日記です。

昨日はこれから作るアプリの部品を仮置きしてみて、イメージを固めるところまでやってみました。

今日はアプリの機能部分の実装に着手していこうと思います。

stateをAppコンポーネントに移譲する

個別のコンポーネントがいったん形になったので、コンポーネントの連携を行います。

まずはFormコンポーネントで管理しているstateAppコンポーネントに移譲します。

Appコンポーネントで状態管理を行うことで、アプリ全体の状態を各コンポーネントに共有できるようにします。

src/App.js

import React from 'react';
import Form from './Form';
import TodoList from './TodoList'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {value: ''}

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(event) {
    this.setState({value: event.target.value})
  }

  render() {
    return (
      <React.Fragment>
        <h1>Todo App</h1>
        <Form value={this.value} handleChange={this.handleChange} />
        <TodoList/>
      </React.Fragment>
    );
  }
}

export default App;

src/Form.js

import React from 'react'

class Form extends React.Component {
    render() {
        return (
            <React.Fragment>
                <label>
                    todo:
                    <input type="text" className="todoform" value={this.props.value} onChange={this.props.handleChange} />
                    <input type="submit" value="Add" />
                </label>
            </React.Fragment>
        );
    }
}

export default Form;

タスク追加機能を実装する

このアプリではAddボタンが押された時にタスクを追加します。
その機能を実現するため、Addボタンのクリックを検知するイベントリスナーhandleClickを定義しましょう。

handleClickメソッド

handleClickはボタンが押された時、次の処理を実行します。

  • リストに新しいタスクを追加する
  • フォームに入力された値をクリアする

リストに追加する処理を実現するため、stateにtodosを追加します。
todosはタスクのリストを管理する配列で、ボタンがクリックされた時にフォームの値をこの配列に追加するようにします。

そしてhandleClickでstateの各値を更新することで、上の2つの処理を実現させます。

これまでの話を取り込んだ結果が以下のコードとなります。

src/App.js

import React from 'react';
import Form from './Form';
import TodoList from './TodoList'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {value: '' ,todos: []}

    this.handleChange = this.handleChange.bind(this)
    this.handleClick = this.handleClick.bind(this)
  }

  handleChange(event) {
    this.setState({value: event.target.value})
  }

  handleClick() {
    const newTodo = this.state.todos.slice()
    newTodo.push(this.state.value)
    this.setState({value: '', todos: newTodo})
  }

  render() {
    return (
      <React.Fragment>
        <h1>Todo App</h1>
        <Form value={this.state.value} handleChange={this.handleChange} handleClick={this.handleClick} />
        <TodoList/>
      </React.Fragment>
    );
  }
}

export default App;
import React from 'react'

class Form extends React.Component {
    render() {
        return (
            <React.Fragment>
                <label>
                    todo:
                    <input type="text" className="todoform" value={this.props.value} onChange={this.props.handleChange} />
                    <input type="submit" value="Add" onClick={this.props.handleClick} />
                </label>
            </React.Fragment>
        );
    }
}

export default Form;

まとめ

これでFormコンポーネントAppコンポーネントでタスク登録機能を実装する準備が整いました。

明日はTodoListコンポーネントを修正し、登録されたタスクが表示されるようにしようと思います。

Reactを使ってTodoアプリを作ってみる_1日目

初めに

最近になってフロントエンド開発案件に配属されたのですが、実際に開発するとなるとさっぱり手が動かないということに気付かされました。

事前にチュートリアルを読んで勉強していたのですが、それはただ正解が用意されたコードをなぞっていただけのこと。
いざ開発するとなるとどうすればいいのかわからなくなってしまうのです。

そんなわけで実装力を鍛えようと思い、簡単なTodoアプリの作成をしてみることにしました。

環境構築

Create React Appを使ってさっくりプロジェクトを作成します。
なおCreate React Appを実行するには事前にNode.jsのインストールが必要です。

npx create-react-app react-todo
cd react-todo
npm start

プロジェクトの作成が正常に完了すれば下の画面が立ち上がります。

f:id:Amsel1676:20200318080840p:plain
React初期画面

作成するアプリについて

まずは必要最低限の機能だけもったTodoアプリの作成を目指します。
今回実装するのは以下の機能です。

  • タスクを登録する
  • タスクを削除する
  • タスクを完了状態にする

アプリの構成

以下の3つのコンポーネントでアプリを構築していきます。

Appコンポーネントはアプリ全体の状態を管理します。
FormコンポーネントはTodoを入力するフォームと登録のボタン部分を担当します。
TodoListコンポーネントは入力されたTodoをリスト化して表示する部分を担当します。

アプリの作成

まずはcreate-react-appで作成されたファイルを整理します。 create-react-appが生成するソースの内、使わないファイルは削除してしまいましょう。

今回はsrcフォルダ配下のApp.jsindex.jsserviceWorker.js以外は削除します。

formコンポーネントの作成

まずは形から作成していきましょう。

このアプリのフォームを構成するFormコンポーネントを定義し、入力フォームとそのフォームに入力されたタスクを登録するためのボタンを仮作成します。

src/Form.js

import React from 'react'

class Form extends React.Component {
    constructor(props) {
        super(props)
        this.state = {value: ''}

        this.handleChange = this.handleChange.bind(this)
    }

    handleChange(event) {
        this.setState({value: event.target.value})
    }

    render() {
        return (
            <React.Fragment>
                <label>
                    todo:
                    <input type="text" className="todoform" value={this.state.value} onChange={this.handleChange} />
                    <input type="submit" value="Add" />
                </label>
            </React.Fragment>
        );
    }
}

export default Form;

このFormコンポーネントAppコンポーネント側で呼び出し、表示されるか確認してみます。

src/App.js

import React from 'react';
import Form from './Form';

function App() {
  return (
    <React.Fragment>
      <h1>Todo App</h1>
      <Form/>
    </React.Fragment>
  );
}

export default App;

無事、フォームとボタンが表示されました。

f:id:Amsel1676:20200505183307p:plain
フォーム追加後

TodoListコンポーネントの作成

次は登録されたTodoを表示するリストを作成します。

こちらもまずは形から。 登録フォームはまだ機能していないので、静的なリストを仮置きしてみます。

src/TodoList.js

import React from 'react'

class TodoList extends React.Component {
    render() {
        return (
            <ul>
                <li><input type="checkbox" /> Temp 1 <input type="submit" value="delete" /></li>
                <li><input type="checkbox" /> Temp 2 <input type="submit" value="delete" /></li>
                <li><input type="checkbox" /> Temp 3 <input type="submit" value="delete" /></li>
            </ul>
        );
    }
}

export default TodoList;

App.jsからTodoListコンポーネントを呼び出し、正しく表示されるか確認してみます。

src/App.js

import React from 'react';
import Form from './Form';
import TodoList from './TodoList';

function App() {
  return (
    <React.Fragment>
      <h1>Todo App</h1>
      <Form/>
      <TodoList/>
    </React.Fragment>
  );
}

export default App;

f:id:Amsel1676:20200505183351p:plain
リスト追加後

無事表示されました。

まとめ

今日はとりあえず形だけ作成してみました。
明日は実際にタスクを登録する機能を追加するところまでやる予定です。

この程度の実装でしたが、躓いた所がいくつかありました。
特にthisのbindを忘れてhandleChangeメソッドが認識されなかった事が大きなハマりポイントでしたので、JavaScriptのthisについて勉強しなおさないといけませんね…。

参考文献

ja.reactjs.org