【React】TODOアプリを作成する

Reactの習得にあたって、こちらのUdemyの教材で学習をした内容についてまとめました。

結論めちゃめちゃわかりやすいのでおすすめです!

この講座はほんとにいいです!これまで何をやってもReactを全然理解できなかった中、辿り着いたこちらの講座に本当に救われました。

【教育・学習】資格・学習
総合情報サイト「コレダ!」がお届けする教育・学習における資格・学習の総合情報サイトです。

ReactでTODOアプリを作ります。

では行きましょう!

Reactで状態が変化するものはstateで定義するんでしたね!まずはそこからいきます!

stateの定義

未完了のstateを作り、初期値に「タスク1」と「タスク2」を入れます。

  const [incompleteTodos, setIncompleteTodos] = useState([
    "タスク1",
    "タスク2"
  ]);

map関数を使い、引数をtodoとし、incompleteTodosの配列の中から値を順に取り出します。

      <div className="incomplete-area">
        <p>未完了のTODO</p>
        <ul>
          {incompleteTodos.map((todo) => {
            return (
              <div>
                <li>{todo}</li>
                <button>完了</button>
                <button>削除</button>
              </div>
            );
          })}
        </ul>
      </div>

key

Reactでループ処理を行う場合、ループ内で返却している一番親のタグにkeyを設定する必要がある。

これは、仮想DOMでは差分のみ反映していくため、何番目のものであるかがわかるようにするために必要になる。

ちなみにkeyの設定が無いと、以下のエラーとなる。

Warning: Each child in a list should have a unique "key" prop.

keyを設定した状態がこちら

          {incompleteTodos.map((todo) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button>完了</button>
                <button>削除</button>
              </div>
            );
          })}

フォームに入力したテキストを”未完了のTODO”に追加する

TODOの入力フォームを作成し、投稿したら未完了のTODOに追加されるようにします。

初期値と入力値を作る

  • 入力値のvalueを変数todoTextとして、stateを作成する
  • onChangeで、inputの変更の検知をする
  • onChangeTodoText関数を設定し、event引数を受け取り、setTodoTextの関数に反映する
export const App = () => {
  const [todoText, setTodoText] = useState("");
  const onChangeTodoText = (event) => setTodoText(event.target.value);
  return (
    <>
      <div className="input-area">
        <input
          placeholder="TODOを入力"
          value={todoText}
          onChange={onChangeTodoText}
        />
        <button>追加</button>
      </div>

  省略

「追加」ボタンクリックで未完了リストに追加する

onClickAddが発火した時に、todoTextの値をincompleteTodosの配列に追加していく。

  • 新たに追加される配列をnewTodosとする
  • 更新関数setIncompleteTodosにnewTodosを設定
  • 「追加」後にフォームを空にする処理と、空では追加できないようにする条件式の追加
  const onClickAdd = () => {
    if (todoText === "") return;
    const newTodos = [...incompleteTodos, todoText];
    setIncompleteTodos(newTodos);
    setTodoText("");
  };

  return (
    <>
      <div className="input-area">
        <input
          placeholder="TODOを入力"
          value={todoText}
          onChange={onChangeTodoText}
        />
        <button onClick={onClickAdd}>追加</button>
      </div>

削除機能追加

incompleteTodosの配列から、対象のリストを削除する。

  • 削除ボタンにonClickイベントを割り当て、クリックされたときの関数をonClickDeleteと定義する
  • 何行目がクリックされたかがわかるよう引数にindexを設定する
    incompleteTodos のmapで第二引数にindexを設定する。

関数に引数を渡す場合はアロー関数で新しく関数を生成する必要がある。

<button onClick={onClickDelete(index)}>削除</button>

こうすると、この時点で関数が実行されてしまうため、常に表示されてしまう。
削除ボタンが実装された時に実行されてほしいので、アロー関数を書いて新しく関数を生成してあげるような処理とする。

<button onClick={() => onClickDelete(index)}>削除</button>

一旦alertで表示して確認してみる。

  const onClickDelete = (index) => {
    alert(index);
  };

  省略

  return (
    <>
  省略
      <div className="incomplete-area">
        <p>未完了のTODO</p>
        <ul>
          {incompleteTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button>完了</button>
                <button onClick={() => onClickDelete(index)}>削除</button>
              </div>
            );
          })}
        </ul>
      </div>

あとは、新しく配列を生成し、その中から指定したindexが削除されるように処理をする。

配列からの要素の削除にはspliceを使用します。第一引数に削除する要素の番号、第二引数に削除数を入れる。

最後に、更新関数setIncompleteTodosにnewTodosを入れる。

  const onClickDelete = (index) => {
    const newTodos = [...incompleteTodos];
    newTodos.splice(index, 1);
    setIncompleteTodos(newTodos);
  };

完了機能

削除ボタンと同様にボタンクリックで”未完了のTODO”から削除するとともに、

“完了のTODO”に追加する。

  const onClickComplete = (index) => {
    const newIncompleteTodos = [...incompleteTodos];
    newIncompleteTodos.splice(index, 1);

    const newCompleteTodos = [...completeTodos, incompleteTodos[index]];
    setIncompleteTodos(newIncompleteTodos);
    setCompleteTodos(newCompleteTodos);
  };

  return (
    <>
    省略
      <div className="incomplete-area">
        <p>未完了のTODO</p>
        <ul>
          {incompleteTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button onClick={() => onClickComplete(index)}>完了</button>
                <button onClick={() => onClickDelete(index)}>削除</button>
              </div>
            );
          })}
        </ul>
      </div>

以下の部分だけ解説

const newCompleteTodos = [...completeTodos, incompleteTodos[index]];

第一引数に、今ある”完了のTODO”の要素を、第二引数に今クリックした要素を設定する。

これで新しい”完了のTODO”の配列ができる。

戻す機能

「完了」から「未完了」に戻す処理を実装していきます。

  • ボタンにonClickイベントを割り当て、関数onClickBackと引数indexを割り当てる
  • 関数onClickBackを設定
    新しく作られる”完了のTODO”newCompleteTodosに今のcompleteTodosを代入
    newCompleteTodos の要素をspliceで削除していく
  • ”未完了のTODO”の newIncompleteTodos に追加していく
  • それぞれ更新関数を呼びたし、再設定する
  const onClickBack = (index) => {
    const newCompleteTodos = [...completeTodos];
    newCompleteTodos.splice(index, 1);

    const newIncompleteTodos = [...incompleteTodos, completeTodos[index]];
    setIncompleteTodos(newIncompleteTodos);
    setCompleteTodos(newCompleteTodos);
  };

  省略

          {completeTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button onClick={() => onClickBack(index)}>戻す</button>
              </div>
            );
          })}

ここまでのまとめ

コンポーネント化する前の状態でのここまでの全コードです。

次からコンポーネント化し、分解していきます。

import React, { useState } from "react";
import "./styles.css";

export const App = () => {
  const [todoText, setTodoText] = useState("");
  const [incompleteTodos, setIncompleteTodos] = useState([]);
  const [completeTodos, setCompleteTodos] = useState([]);

  const onChangeTodoText = (event) => setTodoText(event.target.value);

  const onClickAdd = () => {
    if (todoText === "") return;
    const newTodos = [...incompleteTodos, todoText];
    setIncompleteTodos(newTodos);
    setTodoText("");
  };

  const onClickComplete = (index) => {
    const newIncompleteTodos = [...incompleteTodos];
    newIncompleteTodos.splice(index, 1);

    const newCompleteTodos = [...completeTodos, incompleteTodos[index]];
    setIncompleteTodos(newIncompleteTodos);
    setCompleteTodos(newCompleteTodos);
  };

  const onClickDelete = (index) => {
    const newTodos = [...incompleteTodos];
    newTodos.splice(index, 1);
    setIncompleteTodos(newTodos);
  };

  const onClickBack = (index) => {
    const newCompleteTodos = [...completeTodos];
    newCompleteTodos.splice(index, 1);

    const newIncompleteTodos = [...incompleteTodos, completeTodos[index]];
    setIncompleteTodos(newIncompleteTodos);
    setCompleteTodos(newCompleteTodos);
  };

  return (
    <>
      <div className="input-area">
        <input
          placeholder="TODOを入力"
          value={todoText}
          onChange={onChangeTodoText}
        />
        <button onClick={onClickAdd}>追加</button>
      </div>

      <div className="incomplete-area">
        <p>未完了のTODO</p>
        <ul>
          {incompleteTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button onClick={() => onClickComplete(index)}>完了</button>
                <button onClick={() => onClickDelete(index)}>削除</button>
              </div>
            );
          })}
        </ul>
      </div>

      <div className="complete-area">
        <p>完了のTODO</p>
        <ul>
          {completeTodos.map((todo, index) => {
            return (
              <div key={todo} className="list-row">
                <li>{todo}</li>
                <button onClick={() => onClickBack(index)}>戻す</button>
              </div>
            );
          })}
        </ul>
      </div>
    </>
  );
};

コンポーネント化

“TODOの追加部”と、”未完了のTODO”、”完了のTODO”の3つにコンポーネントを分ける。

componentsディレクトリを作成し、3つのコンポーネントファイルを作成する。

ファイル名は、コンポーネント名と同じのため、パスカルケースとする。

“TODOの追加部” のコンポーネント化

入力フォームのエリアを移行します。

ただ、そのまま移行すると設定した関数が無いため、エラーとなってしまうので、propsを使って、stateや関数等を渡す。

ただ移行しただけの状態

const InputTodo = () => {
  return (
    <div className="input-area">
      <input
        placeholder="TODOを入力"
        value={todoText}
        onChange={onChangeTodoText}
      />
      <button onClick={onClickAdd}>追加</button>
    </div>
  );
};

ここにpropsを渡していく。

InputTodoファイル内で定義されていない関数は以下の3つ

todoText, onChangeTodoText, onClickAdd

App.jsファイルで以下のように記述し渡す。

  return (
    <>
      <InputTodo todoText={todoText} onChange={onChangeTodoText} onClick={onClickAdd}/>

InputTodoファイル に、分割代入で渡していく。

export const InputTodo = (props) => {
  const { todoText, onChange, onClick } = props;
  return (
    <div className="input-area">
      <input placeholder="TODOを入力" value={todoText} onChange={onChange} />
      <button onClick={onClick}>追加</button>
    </div>
  );
};

“未完了のTODO” のコンポーネント化

何をpropsとして渡す必要があるか?

stateのincompleteTodos

関数のonClickCompleteとonClickDelete

      <IncompleteTodos
        todos={incompleteTodos}
        onClickComplete={onClickComplete}
        onClickDelete={onClickDelete}
      />

export const IncompleteTodos = (props) => {
  const { todos, onClickComplete, onClickDelete } = props;
  return (
    <div className="incomplete-area">
      <p>未完了のTODO</p>
      <ul>
        {todos.map((todo, index) => {
          return (
            <div key={todo} className="list-row">
              <li>{todo}</li>
              <button onClick={() => onClickComplete(index)}>完了</button>
              <button onClick={() => onClickDelete(index)}>削除</button>
            </div>
          );
        })}
      </ul>
    </div>
  );
};

“完了のTODO” のコンポーネント化

同様に。

コンポーネント分割したのがこちら。

<CompleteTodos todos={completeTodos} onClickBack={onClickBack} />

export const CompleteTodos = (props) => {
  const { todos, onClickBack } = props;
  return (
    <div className="complete-area">
      <p>完了のTODO</p>
      <ul>
        {todos.map((todo, index) => {
          return (
            <div key={todo} className="list-row">
              <li>{todo}</li>
              <button onClick={() => onClickBack(index)}>戻す</button>
            </div>
          );
        })}
      </ul>
    </div>
  );
};

CSS-in-js

コンポーネントを分けた部分のCSSも、その分けたコンポーネントファイルに記述すること。

ただ、これは必ずしもする必要は無く開発チームによって使用するかどうかは異なる。

以下のInputTodoコンポーネントに以下のCSSを適用する場合

export const InputTodo = (props) => {
  const { todoText, onChange, onClick } = props;
  return (
    <div className="input-area">
      <input placeholder="TODOを入力" value={todoText} onChange={onChange} />
      <button onClick={onClick}>追加</button>
    </div>
  );
};

.incomplete-area {
  background-color: #c6ffe2;
}

CSSをコンポーネントに記述する時の注意点

  • -の繫ぎをキャメルケースに変更
  • 値を文字列に変更
  • 最後のセミコロンをカンマに変更(オブジェクトという扱いだから)

divのところに指定しているclassNameは不要となり、styleでCSSを適用する。

移行後

const style = {
  backgroundColor: "#c1ffff"
};

export const InputTodo = (props) => {
  const { todoText, onChange, onClick } = props;
  return (
    <div style={style}>
      <input placeholder="TODOを入力" value={todoText} onChange={onChange} />
      <button onClick={onClick}>追加</button>
    </div>
  );
};

バリデーション・メッセージ

TODOリストは5個までしか保存できないというようにする。

  1. 5個登録された際に表示されるメッセージを用意
  2. incompleteTodosを基に条件式を書く
  3. 条件の5個に達したらinputエリアとボタンを非活性にする。非活性はdisabledを使う。
{incompleteTodos.length >= 5 && <p>登録は5個まで</p>}

4. propsとしてdisabledを受け取り、trueならdisabledにする(メソッドが機能する)ようにする。

export const InputTodo = (props) => {
  const { todoText, onChange, onClick, disabled } = props;
  return (
    <div style={style}>
      <input
        disabled={disabled}
        placeholder="TODOを入力"
        value={todoText}
        onChange={onChange}
      />
      <button disabled={disabled} onClick={onClick}>
        追加
      </button>
    </div>
  );
};

5. App.jsのJSX内で以下のようにpropsを設定することで、5以上の時disableはtrueとなる

disabled={incompleteTodos.length >= 5}

以上、ReacrtでTODOをつくりました。

削除から戻す作成、削除、戻し機能があり、結構しっかりしたTODOを作成しました。

これでReactの基礎をマスターしました!

ご覧いただきありがとうございました。

コメント