1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Reactでアプリを作成しました【5】【Todo List ②】

Last updated at Posted at 2021-09-28

##【仕様】

  • テキスト入力欄にタスクを書き込んでエンターキーを押すと下のリストに追加される。
  • 完了したタスクは、チェックボックスを『ON』にする。視覚効果として、完了済みのタスクは文字を薄くなる。
  • タブによるフィルタリング機能もあり、『All』を選択すると全てのタスク、『ToDo』を選択すると未完了のタスクのみ、『Done』を選択すると完了済みのタスクのみが表示される。
  • 下部には表示中のタスク件数が表示される。

##【コンポーネント設計する】

#####アプリの作成(実装)を始める前に、どうアプリを作成するのかを下記の点を踏まえ、考える。

  • どのようにコンポーネントを分けるか。
  • それぞれのコンポーネントはどのような属性(props)を受け取り、どのような状態(state)を管理するか。

#####※ 『フォーム』や『ボタン』『リンク』『タブ』などインタラクティブな UI 要素がある場合、『ハンドラ』が必要。 誰が『ハンドラ』を定義して、誰が実行するかを考えること。

##1. 作成する『Todoアプリ』のコンポーネントを設計する
#####① 『Todo』コンポーネントは、アプリの全体 
➡︎ アプリの大元になるので、外部からの props は不要。
#####② 『Input』コンポーネントは、タスクの入力欄
➡︎ 入力欄なので、入力値を『state』として管理する。この入力値は、エンターキーが押されるまで親(Todo)が知る必要はないので、『Input』コンポーネント内で管理するが、エンターキーが押されたら、親のタスクリストを更新する必要がある。
#####③ 『Filter』コンポーネントは、 タブのフィルタリング部分
➡︎ 『Input』と同様、それぞれのタブが押された時のイベントハンドラ関数を『props』 として受け取る必要がある。そのほか、選択中のタブは見た目を変える必要がある。

#####④ 『TodoItem』 コンポーネントは、タスク一個分
➡︎ 一個分のタスク情報を親から『props』で渡してもらう必要がある。さらに、チェックボックスがあり、チェックすると、何らかの形でタスクの完了状態を更新する。『TodoItem』はタスク情報をもらうだけで、自分では管理しない。管理するのは『 Todo』で、子でイベントが発生したタイミングで、親の『state』が更新される必要があるので、『Todo』から『TodoItem』にイベントハンドラを渡すパターンが良い。言い換えるなら、『TodoItem』は、どのタスクがチェックされたか、親である『Todo』 知らせる必要がある。
#####※「知らせる」とは「イベント」のこと

###Propsとは
[Reactを基本からまとめてみた【5】【Props(プロップス)】]
(https://qiita.com/kanfutrooper/items/c0e75dc3c2ad8cbd32e5)

###Stateとは
[Reactを基本からまとめてみた【6】【State(ステート)】]
(https://qiita.com/kanfutrooper/items/340760f49fc253f612da)

##2. HTMLを作成する

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>⚛️ React ToDo</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.2/css/bulma.min.css" />
  <style>
    .container { margin-top: 2rem; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

  <script type="text.js">

    // ここにコードを書いていく
  
  </script>
</body>
</html>

###CSS フレームワーク 『Bulma』 とは
公式サイト : [Bulma]
(https://bulma.io/)

『Bulma』は、 JavaScript 無しのプレーンな CSS フレームワーク。予め用意されたコンポーネントに対応するクラス名をHTML要素に付与することでスタイリングを行う。

###React ライブラリ 『classnames』 とは
パッケージドキュメント : [classnames]
(https://www.npmjs.com/package/classnames#usage-with-reactjs)
htmlではclassを使ってスタイリングをするが、Reactが使っているjsxではclassNameを使ったスタイリングを行う。

##3. レンダリングされるルートコンポーネントとして『App』を作成する

text.js
function Todo() {
  return null;
}

function App() {
  return (
    <div className="container is-fluid">
      <Todo />
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

##4. タスクを表示する機能を作成する

#####①Todo に以下の state を生成する。
タスクは複数存在するので、配列で表現する。

const [items, setItems] = React.useState([]);

この配列に入るのはデータは、タスクの文字列をそのまま配列に入れると、完了したかどうか分からないので、以下の形式で管理する。

{
  key: String,
  text: String,
  done: Boolean
}

key は、タスクを一意に特定する ID で、本格的なアプリだとデータベースに格納した結果の ID 値などになるが、今回は以下の関数でランダムな文字列を生成する。
この関数はコードの一番上に追加する。

const getKey = () => Math.random().toString(32).substring(2);

#####② state の初期値にテストデータを入れる。
その state をループで表示する JSX コードを追加する。

text.js
function Todo() {
  const [items, setItems] = React.useState([
    { key: getKey(), text: 'Learn JavaScript', done: false },
    { key: getKey(), text: 'Learn React', done: false },
    { key: getKey(), text: 'Get some good sleep', done: false },
  ]);

  return (
    <div className="panel">
      <div className="panel-heading">
        ⚛️ React ToDo
      </div>
      {items.map(item => (
        <label className="panel-block">
          <input type="checkbox" />
          {item.text}
        </label>
      ))}
      <div className="panel-block">
        {items.length} items
      </div>
    </div>
  );
}

#####③ タスクの部分を『TodoItem』コンポーネントに切り出す。

function TodoItem({ item }) {
  return (
    <label className="panel-block">
      <input type="checkbox" />
      {item.text}
    </label>
  );
}

TodoItem から返される JSX は以下のようになる。
ループで生成される要素には『key』が必要で、タスクに用意した『key』プロパティを利用する。

<div className="panel">
  <div className="panel-heading">
    ⚛️ React ToDo
  </div>
  {items.map(item => (
    <TodoItem key={item.key} item={item} />
  ))}
  <div className="panel-block">
    {items.length} items
  </div>
</div>

##5.タスクの完了状態を切り替える
#####①チェックボックスで完了状態を切り替えられるようにする。
TodoItem では、チェックされた(もしくは外された)時に、ハンドラ関数 onCheck を実行する。ハンドラにはタスク情報を渡す。子から親に知らせるイメージ。

function TodoItem({ item, onCheck }) {
  const handleChange = () => {
    onCheck(item);
  };

  return (
    <label className="panel-block">
      <input
        type="checkbox"
        checked={item.done}
        onChange={handleChange}
      />
      {item.text}
    </label>
  );
}

#####②Todo にハンドラを実装する。
items から map で新しいリストを作成して setItems する。map の中では、key で同一判定をして、チェック対象の done の真偽を反転させる。

const handleCheck = checked => {
  const newItems = items.map(item => {
    if (item.key === checked.key) {
      item.done = !item.done;
    }
    return item;
  });
  setItems(newItems);
};

#####③実装したハンドラを TodoItem の onCheck props に指定する。

{items.map(item => (
  <TodoItem
    key={item.key}
    item={item}
    onCheck={handleCheck}
  />
))}

#####④TodoItem の JSXをBulma のヘルパークラスを用い、完了済みのタスクは文字色を灰色に変化するように編集する。

<label className="panel-block">
  <input
    type="checkbox"
    checked={item.done}
    onChange={handleChange}
  />
  <span
    className={classNames({
      'has-text-grey-light': item.done
    })}
  >
    {item.text}
  </span>
</label>

#####⑤ {item.text} に CSS クラスを適用させるために で囲った上で、classnames ライブラリを利用する。

※ IとIIは同じ意味

I
className={classNames({
  'has-text-grey-light': item.done // 真偽値
})}
II
className={item.done ? 'has-text-grey-light' : ''}

######⑥TodoItem コンポーネントは完成。
チェックの切り替えと、ON 時の文字色の変化を確認する。

##6.タスクを作成する
#####① 入力欄の値を管理するだけの Input コンポーネントを作成する。

function Input() {
  const [text, setText] = React.useState('');

  const handleChange = e => setText(e.target.value);

  return (
    <div className="panel-block">
      <input
        class="input"
        type="text"
        placeholder="Enter to add"
        value={text}
        onChange={handleChange}
      />
    </div>
  );
}

#####② エンターキーを押したときのハンドラを実装する。
onKeyDown のハンドラで一旦イベントを受け取り、エンターキーであった時のみ props で受け取る想定の onAdd を実行する。引数には入力されたテキストを渡す。さらに、追加処理の後は入力欄をクリアにする。

function Input({ onAdd }) {
  // 中略

  const handleKeyDown = e => {
    if (e.key === 'Enter') {
      onAdd(text);
      setText('');
    }
  };

  return (
    <div className="panel-block">
      <input
        class="input"
        type="text"
        placeholder="Enter to add"
        value={text}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
}

#####⑤Todo に、追加処理を行うハンドラ関数を実装する。
テキストは子から渡されるので、key と done を追加して、タスクリストに追加する。

const handleAdd = text => {
  setItems([...items, { key: getKey(), text, done: false }]);
};

######⑥.panel-heading の下に Input を配置する。

<Input onAdd={handleAdd} />

##7.フィルタリング機能を作成する
#####①Todo に state を追加する。
フィルタリング条件は、ALL / TODO / DONE の文字列で、今回は表現する。

const [filter, setFilter] = React.useState('ALL');

######② Filter コンポーネントを実装する。

function Filter({ value, onChange }) {
  const handleClick = (key, e) => {
    e.preventDefault();
    onChange(key);
  };

  return (
    <div className="panel-tabs">
      <a
        href="#"
        onClick={handleClick.bind(null, 'ALL')}
      >All</a>
      <a
        href="#"
        onClick={handleClick.bind(null, 'TODO')}
      >ToDo</a>
      <a
        href="#"
        onClick={handleClick.bind(null, 'DONE')}
      >Done</a>
    </div>
  );
}

###bind() メソッドとは
bind() メソッドは、呼び出された際に this キーワードに指定された値が設定される新しい関数を生成。この値は新しい関数が呼び出された時、一連の引数の前に置かれる。

#####③Todo に戻り、フィルタリング条件を更新する関数を作成する。

const handleFilterChange = value => setFilter(value);

#####④items を直接表示するのではなく、条件に応じてフィルタリングされた結果を表示する。
以下のコードを Todo に追加する。

const displayItems = items.filter(item => {
  if (filter === 'ALL') return true;
  if (filter === 'TODO') return !item.done;
  if (filter === 'DONE') return item.done;
});

#####⑤タスク表示箇所と件数表示箇所を displayItems を使うように編集する。

displayItems.map(item => (
  // 中略
))}
<div className="panel-block">
  {displayItems.length} items
</div>

#####⑥タブの表示切り替えを実装する。
classnames を使用する。

<a
  href="#"
  onClick={handleClick.bind(null, 'ALL')}
  className={classNames({ 'is-active': value === 'ALL' })}
>All</a>
<a
  href="#"
  onClick={handleClick.bind(null, 'TODO')}
  className={classNames({ 'is-active': value === 'TODO' })}
>ToDo</a>
<a
  href="#"
  onClick={handleClick.bind(null, 'DONE')}
  className={classNames({ 'is-active': value === 'DONE' })}
>Done</a>

##7.完成

const getKey = () => Math.random().toString(32).substring(2);

function Todo() {
  const [items, setItems] = React.useState([]);
  const [filter, setFilter] = React.useState('ALL');

  const handleAdd = text => {
    setItems([...items, { key: getKey(), text, done: false }]);
  };

  const handleFilterChange = value => setFilter(value);

  const displayItems = items.filter(item => {
    if (filter === 'ALL') return true;
    if (filter === 'TODO') return !item.done;
    if (filter === 'DONE') return item.done;
  });

  const handleCheck = checked => {
    const newItems = items.map(item => {
      if (item.key === checked.key) {
        item.done = !item.done;
      }
      return item;
    });
    setItems(newItems);
  };

  return (
    <div className="panel">
      <div className="panel-heading">
        ⚛️ React ToDo
      </div>
      <Input onAdd={handleAdd} />
      <Filter
        onChange={handleFilterChange}
        value={filter}
      />
      {displayItems.map(item => (
        <TodoItem
          key={item.text}
          item={item}
          onCheck={handleCheck}
         />
      ))}
      <div className="panel-block">
        {displayItems.length} items
      </div>
    </div>
  );
}

function Input({ onAdd }) {
  const [text, setText] = React.useState('');

  const handleChange = e => setText(e.target.value);

  const handleKeyDown = e => {
    if (e.key === 'Enter') {
      onAdd(text);
      setText('');
    }
  };

  return (
    <div className="panel-block">
      <input
        class="input"
        type="text"
        placeholder="Enter to add"
        value={text}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
}

function Filter({ value, onChange }) {
  const handleClick = (key, e) => {
    e.preventDefault();
    onChange(key);
  };

  return (
    <div className="panel-tabs">
      <a
        href="#"
        onClick={handleClick.bind(null, 'ALL')}
        className={classNames({ 'is-active': value === 'ALL' })}
      >All</a>
      <a
        href="#"
        onClick={handleClick.bind(null, 'TODO')}
        className={classNames({ 'is-active': value === 'TODO' })}
      >ToDo</a>
      <a
        href="#"
        onClick={handleClick.bind(null, 'DONE')}
        className={classNames({ 'is-active': value === 'DONE' })}
      >Done</a>
    </div>
  );
}

function TodoItem({ item, onCheck }) {
  const handleChange = () => {
    onCheck(item);
  };

  return (
    <label className="panel-block">
      <input
        type="checkbox"
        checked={item.done}
        onChange={handleChange}
      />
      <span
        className={classNames({
          'has-text-grey-light': item.done
        })}
      >
        {item.text}
      </span>
    </label>
  );
}

function App() {
  return (
    <div className="container is-fluid">
      <Todo />
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

##参考サイト
[React入門チュートリアル (5) ToDoアプリを作ってみよう]
(https://www.hypertextcandy.com/react-tutorial-05-wrap-up-with-todo-app)
[CSSフレームワークのススメ - BULMAの導入と覚え書き]
(https://qiita.com/belq/items/10ec41f656e47ee2b540)
[CSS フレームワーク Bulma チートシート]
(https://blog1.mammb.com/entry/2021/06/20/235459)
[Reactのスタイリング(classNameやclassNamesの使い方)]
(https://qiita.com/terry_6518/items/de53123253cf67e048b3)

1
0
1

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?