LoginSignup
10
3

More than 3 years have passed since last update.

【初心者向け】ReactでToDoアプリを作ってみた

Last updated at Posted at 2020-05-24

概要

今回はアプリ制作の登竜門とも言える、ToDoアプリを作成してみました。
実はこれ、単純に見えて意外と複雑!
でもReactでstate・propsなどの流れを確認するにはちょうどいいレベルですね。

完成形はこちら

ezgif.com-optimize.gif

小さくてすみません。。。

搭載機能

大きく分けるとこんな感じです。
スクリーンショット 2020-05-24 14.34.42.png

開発環境

・macOS Catalina ver.10.15.4
・Editor:VScode
・node.js(create-react-app)

全体図

スクリーンショット 2020-05-24 15.14.12.png

主に使うやつだけ載せました!
ざっとですが、流れとしてはindex.html←index.js←App.js・index.css←componentsって感じです。
ここの位置関係をちゃんと把握しておくと、後々楽になってきます。

解説

今回はUIのパーツごと・ファイルごとにコードの成り立ちを解説している形になっています。
ですので、実際に作っていく流れとは違うとは思います!ご了承を。

1.TodoHeader.jsx
2.TodoForm.jsx
3.TodoList.jsx
4.App.js
5.index.css

1.TodoHeader.jsx

これはリストの最上部を描画しているコンポーネントです。
ここで実装されている主な機能は
・複数で削除が可能、Alert機能
・Check/総数 でカウント
ですね。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
全体のコードはこちら!

JSX.js
import React from 'react';

export default function Todoheader(props) {
    const remaining = props.todos.filter((todo) => {
        return !todo.isDone;
    });
    return (
        <h1>
            <button onClick={props.purge}>Purge</button>
        Today's Task
            <span>
                ({remaining.length}/{props.todos.length})
        </span>
        </h1>
    );
}

remainingではfilterで、isDone=false=□ の反対(!)であるチェックされた項目だけが集められています。これによって{remaining.length}/{props.todos.length}で チェック/全体 を表示しています。
isDone,purge,todosについては4.App.jsからの引用なのでそちらで紹介をします。

2.TodoForm.jsx

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
これはリストの中部を描画しているコンポーネントです。
ここで実装されている主な機能は
・チェックをつけたら斜線が入る
・何もToDoがない時はPerfectを表示
・単独で削除が可能、Alert機能
ですね。
全体のコードはこちら!

JSX.js
import React from 'react';

export default function TodoList(props) {
    const todos = props.todos.map((todo) => {
        return (
            <TodoItem
                key={todo.id}
                todo={todo}
                checkTodo={props.checkTodo}
                deleteTodo={props.deleteTodo}
            />
        );
    });
    return <ul>{props.todos.length ? todos : <li>Perfect!</li>}</ul>;
}

function TodoItem(props) {
    return (
        <li>
            <label>
                <input
                    type="checkbox"
                    checked={props.todo.isDone}
                    onChange={() => props.checkTodo(props.todo)}
                />
                <span className={props.todo.isDone ? "done" : ""}>
                    {props.todo.title}
                </span>
            </label>
            <span className="cmd" onClick={() => props.deleteTodo(props.todo)}>
                [×]
        </span>
        </li>
    );
}

ここではTodoListで要素をコピーし、TodoItemで実際に処理を行っていく流れになります。
まずmapメソッドを用いてtodosをコピーします。その際にTodoItemにkeyやtodoなどの属性を付与するのですが、checkTodo,deleteTodoについては後で見ていきます。

簡単に言うとcheckTodoはチェックボックスを使えるようにするため、deleteTodoは消せるようにするためのメソッドです。
ulでは、要素があったらtodosを表示、なかったらPerfect!を表示できるように演算子を使用します。

最初に出てくるspanではcssで斜線を引けるようにtrueの時にdoneクラスを付与します。
次に出てくるspanではクリックで要素を消せるように、onClickで機能を追加しています。

3.TodoList.jsx

これはリストの最下部を描画しているコンポーネントです。
ここで実装されている主な機能は
・入力+Add ボタンで追加が可能
ですね。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
全体のコードはこちら!

JSX.js
import React from 'react';

export default function TodoForm(props) {
    return (
        <form onSubmit={props.addTodo}>
            <input type="text" value={props.item} onChange={props.updateItem} />
            <input type="submit" value="Add" />
        </form>
    );
}

ここではaddTodo,updateItemメソッドを用いて実際に記入した要素が反映されるようにしていますね。
これもApp.jsで見ていきましょう(丸投げ)

4.App.js

ここでは上3つのコンポーネントが集まり、stateを用いた機能を補完しています
さらにプラスで実装されている主な機能は
・リロードしても記録が残る
ですね。
全体のコードはこちら!
このコンポーネントは複雑なので注釈をつけていくスタイルにします。

JSX.js
import React from "react";
import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
import Todoheader from "./components/Todoheader";

const todos = [];

function getUniqueId() {
  //乱数を発生させて、Itemに一意の番号を付与しています
  return new Date().getTime().toString(36) + Math.random().toString(36);
}

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      todos: todos,
      item: "",
    };
    this.deleteTodo = this.deleteTodo.bind(this);
    this.checkTodo = this.checkTodo.bind(this);
    this.updateItem = this.updateItem.bind(this);
    this.addTodo = this.addTodo.bind(this);
    this.purge = this.purge.bind(this);
  }

  purge() {
    if (!window.confirm("Are you sure?")) {
      //falseならreturnを返す
      return;
    }
    const todos = this.state.todos.filter((todo) => {
      //Trueのときの判定
      //filterでToDoにtodosの1つ1つが入っていって
      //まだチェックしていないToDoだけを集めて更新する
      // =>チェックされている項目だけが消えているように見える
      return !todo.isDone;
    });
    this.setState({
      todos: todos,
    });
  }

  addTodo(e) {
    e.preventDefault(); //(*1)

    if (this.state.item.trim() === "") {
      return; //空文字を処理しない(ToDoに何もない要素が追加されないようにする)
    }

    const item = {
      id: getUniqueId(),
      title: this.state.item,
      isDone: false, // t/fを判断する箱。
                     // 初期状態をfalseにすることで、チェックボックスが□で出てくる
    };

    const todos = this.state.todos.slice();
    //オブジェクトのプロパティをいじらない時のコピーなのでslice
    todos.push(item);
    this.setState({
      todos: todos,
      item: "", //更新した時に空にする
    });
  }

  checkTodo(todo) {
    const todos = this.state.todos.map((todo) => {
      return { id: todo.id, title: todo.title, isDone: todo.isDone };
    });
    //オブジェクトのコピーはmapで行う。todosはstateなので直接変更できない

    const pos = this.state.todos
      .map((todo) => {
        return todo.id;
      })
      .indexOf(todo.id);
    //idのみのtodoをmapで配列に集め、indexOfで渡されてきたtodoが何番目かを最終的な値とする

    todos[pos].isDone = !todos[pos].isDone;
    //取ってきた値のisDone(t/f判定)が反転できるようにする

    this.setState({
      todos: todos,
    });
    //それらを全てstateに反映する
  }

  deleteTodo(todo) {
    if (!window.confirm("Are you sure?")) {
      return;
    }
    const todos = this.state.todos.slice(); 
   //オブジェクトのプロパティをいじらない時のコピーなのでslice
    const pos = this.state.todos.indexOf(todo);
    todos.splice(pos, 1); //pos番目の要素を1つ取り除く
    this.setState({
      todos: todos,
    });
  }

  updateItem(e) {
    this.setState({
      item: e.target.value,
    });
//formの値はイベントオブジェクトから取得できるので、eを引数にしつつthis.setState()として、
//stateの中のitemはformのtarget.valueとするとformに入力された値がUIに反映される
  }

//(*2)ここはリロードしても値を保持するデータの永続化を行っています。
  componentDidUpdate() {
    localStorage.setItem("todos", JSON.stringify(this.state.todos));
  }//ここでlocalStorageに値を保持し
  componentDidMount() {
    this.setState({
      todos: JSON.parse(localStorage.getItem("todos")) || [],
    });//ここで値を読み込ませています
  }

  render() {
    return (
      <div className="container">
        <Todoheader
          todos={this.state.todos}
          purge={this.purge}
        />
        <TodoList
          todos={this.state.todos}
          checkTodo={this.checkTodo}
          deleteTodo={this.deleteTodo}
        />
        <TodoForm
          item={this.state.item}
          updateItem={this.updateItem}
          addTodo={this.addTodo}
        />
      </div>
    );
  }
}

export default App;

*1:(https://qiita.com/tochiji/items/4e9e64cabc0a1cd7a1ae)
*2:(https://qiita.com/jima-r20/items/73b78c4c8cf5af2fed58)

5.index.css

ここでは全体の見た目を整えています。
今回は特にフレームワーク等を使わずに書いているので、コードだけ載せて省略させていただきます。
全体のコードはこちら!

JSX.js
body {
  font-size: 16px;
  font-family: Arial, Helvetica, sans-serif;
}

.container {
  width: 300px;
  margin: auto;
}

.container h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

.container ul {
  padding: 0;
  list-style: none;
}
.container li {
  line-height: 1.5;
}

.container input[type="checkbox"] {
  margin-right: 8px;
}
.container input[type="text"] {
  padding: 2px;
  margin-right: 5px;
}

h1 > span {
  color: #ccc;
  font-size: 12px;
  font-weight: normal;
  margin-left: 7px;
}

h1 > button {
  float: right;
}

.cmd {
  font-size: 12px;
  cursor: pointer;
  color: #08c;
  margin-left: 5px;
}

.done {
  text-decoration: line-through;
  color: #ccc;
}

終わり

少し後半雑になってしまったのは自分自身、完璧に理解できていないからですね
もっと噛み砕いて説明できるように精進したいなと思います。

10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3