useStateの実装とuuidを用いたユニークなキーの設定

この記事は前回の続きです。Todoアプリを再現する際などはこちらを先に参考にしてください。

https://qiita.com/ysk-s/items/dc8c18de2de8d1154476

前回、”Todo"というコンポーネントでuseStateを用いてtodosという変数に保存されているタスクを表示することは出来た。

ただし、ブラウザ上のコンソール画面にはエラーが表示されている。

![Untitled (27).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/de7b02b8-4995-f148-3ef7-bb6a3fc577a5.png)

これは、Todoのコンポーネントで表示されるタスクにはユニークなキーが割り振られておらず、識別できないためにエラーが起こっている。
そのため、TodoListからTodoを呼び出すときkeyの割り振りも行う。

    return todos.map((todo) => (
        <Todo todo={todo} key={todo}/>
      ));

このようにすると、タスクの名前をキーとしてTodoというコンポーネントが作成される。これでも一応はエラーが起こりにくくなるが、残念ながら重複する名前のタスクが作成されたときエラーが発生する。そのため、このキーの付け方は好ましくない。後に変更するため、一旦これで放っておく。

変数の中に辞書型でデータを保管することも可能である。

    const [todos, setTodos] = useState([{id:1, name: "Todo1", completed: false},]);

このようにすることも可能だ。ただし、このままではmap関数で展開したtodoの要素は辞書型のオブジェクトである。
そのため、その辞書型のオブジェクトのnameの要素を取り出すためには、"{todo.name}"とより細かく指定する必要がある。

そして、todoアプリを実装するのでやはりチェックボックスをつけたい。
"src/Todo.jsx"において、"{todo.name}"の前にlabelタグでinputタグを"checkbox"にして作成する。
このとき、checkedという属性に{todo.completed}を入れると、completedという属性に”true"のとき、チェックボックスにチェックが付いた状態になり、"false"だとチェックボックスが空欄の状態で表示される。属性に”readOnly"にしてこのインプットタグを読み取り専用にするとエラーが起こらない。

ここから分かるように、"completed"という"todo"の属性でタスクの未遂か完了かを管理する。
コードは以下のようになる。

```
const Todo = ({ todo }) => {
  return (
    <div>
        <label>
            <input type="checkbox" checked={todo.completed} readOnly/>
        </label>
        {todo.name}
    </div>
  )
}
```

次にブラウザ上でタスクを追加とすると新しいタスクが追加できるようにする。
「タスクを追加」と表示されるbuttonタグの中にonClickで起動する関数を定める。今回は"handleAddTodo"とする。
そして、関数や変数を定義するのはfunction App内でないとエラーになってしまうので注意する。

定めるべき関数handleAddTodoではinputタグに入力されたテキストを取得する必要がある。それを容易に実現するのが"useRef"というものである。
1行目に記入したimportの部分の”useState"の横に”useRef"と書き加える。”useRef"を用いると簡単にinputタグなどから要素の取得ができる。

    import React, { useState, useRef } from "react";
下のように名前をつけてuseRef()をインスタンス化する。このインスタンス化もApp関数の中で定義してやる。

    const todoNameRef = useRef();

そうしたら、取得したい情報が入力されているinputタグに定義した名前を下のようにref={}の中に記述する。

    <input type="text" ref={todoNameRef}/>

こうすることで先程定義した名前"todoNameRef"が呼び出されたときに、情報の取得が行われる。

buttonタグに定めるonClickで起動する関数に名前をつけてその関数の中身をconsole.log(todoNameRef)としてみる。

```
function App() {
  const [todos, setTodos] = useState([{id:1, name: "Todo1", completed: false},]);

  const todoNameRef = useRef();
  const handleAddTodo = () => {
    //タスクを追加する。
    console.log(todoNameRef);
  };
  return (
  <>
    <TodoList todos={todos} />
    <input type="text" ref={todoNameRef}/>
    <button onClick={handleAddTodo}>タスクを追加</button>
    <button>完了したタスクの削除</button>
    <div>残りのタスク:0</div>
  </>
  );
}
```

こうすることで、「タスクを追加」を押すとconsole上に取得したデータが表示される。

![Untitled (29).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/99e9c871-cf4c-8d23-11cc-6f4550588a5f.png)

実際に、多くの情報が取得することが出来る。

inputタグに入力された情報を取得するためにはcurrentフィールドのvalueという中に含まれているのでそれを取得する。

これだけを表示するために以下のようにコードを書き換える。

```
console.log(todoNameRef.current.value)
```

![Untitled (30).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/d4863d82-5095-5c39-61b8-d21890ee5126.png)

こうすると、きちんとコンソール画面にインプットに入力された文字が表示される。

これを用いてタスクを追加していく。

"useState"を使用するときに定義した"[todos, setTodos]"の”setTodos"を使用していく。

”setTodos"は”todos"の中身を更新、追加、削除したい場合に"setTodos"と定義されているセット関数を用いることで実行される。

```
const handleAddTodo = () => {
    //タスクを追加する。
    const name = todoNameRef.current.value;
    setTodos((prevTodos) => {
      return [...prevTodos, {id: "1", name:name, completed: false}];
    });
		todoNameRef.current.value = null;
  };
```

このようにすると、インプットされた情報をタスクの名前としてtodosに追加される。
setTodosを呼び出すことでtodosを変更することが出来る。
スプレッド構文という書き方を使っていて、ここでのprevTodosは前のtodos、つまり変更前のtodosを表していて、それに対して大括弧[]の中のコンマ,を挟んで右側の要素を追加するという意味になっている。
その後、"todoNameRef"の要素を空にしている。

こうすることでタスクを追加できる。

![Untitled (31).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/79f352fb-f84a-beaa-88bc-a4275ecce93f.png)

ただし、追加する要素のidをすべて1としているため、エラーが発生する。

![Untitled (32).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/d4a21bb9-534a-fb29-2260-8feff3d29dd4.png)

ここで、重複しないユニークなidを実装するために使われるライブラリがあるのでそれを使用する。

https://www.npmjs.com/package/uuid

このサイトのドキュメントに従って、uuidをインストールする。
ターミナル上で以下のコードを打つ。

    npm install uuid

そして、App.jsのimportの並びの最後に以下の記述を追加する。

    import { v4 as uuidv4 } from 'uuid';

そして、"setTodos"のidの部分に"uuidv4()"という記述をする。

```
setTodos((prevTodos) => {
	return [...prevTodos, {id: uuidv4(), name:name, completed: false}];
});
```

そして、keyを設定しているのはTodoList.jsxであるからそこで"key={todo.id}"という記述をする。

```
const TodoList = ({ todos }) => {
  return todos.map((todo) => (
    <Todo todo={todo} key={todo.id}/>
  ));
}
```
![Untitled (33).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/9dcf3ef0-1cea-dd14-0477-21319bdfb181.png)

そうするとエラーを吐かない。

次に完了したタスクのチェックボックスにチェックが付くようにしたい。
そのためにcompletedの属性を"false"から"true"に変更できるようにしなければならない。

それを実行するために、”toggleTodo"という関数を作成する。この関数が実行したら実行されたタスクのidを取得する。
そして、"todos"に処理を加えるが、状態変数である”todos"に直接変更を加えるのはあまりよろしくないので、別の変数"newTodos"にデータを格納して処理を実行する。

```
const toggleTodo = (id) => {
    const newTodos = [...todos];
    const todo = newTodos.find((todo) => todo.id === id);

  };
```

そして、取得したidとnewTodosにある要素の中でidが一致する要素を探し出す。このように、条件式にあう要素を1つずつ取り出して探すのが、find関数の役割。そして条件に合うオブジェクトを”todo"という変数に格納している。

そして、completedの属性を反転させる記述をする。
以下のようにスマートに書ける。

    todo.completed = !todo.completed;

こうすることで、チェックボックスにチェックが付いていないときはこの関数が起動するとチェックが付き、反対にチェックボックスにチェックがついているときはチェックが外れる。

そうしたら、この関数を”TodoList.jsx"に引き渡し、さらに”Todo.jsx"に引き渡す。

そして、input,checkboxの部分にonChangeで”handleTodoClick"のような名前で新しい関数を定義してその中で"toggleTodo"を呼び出す。handleTodoClickの関数の中で”todo.id”というようにタスクごとに固有にあるidを取得するようにしてtoggleTodoの引数にする。

よって、src/Todo.jsxは以下のようになる。

```
import React from 'react'

const Todo = ({ todo, toggleTodo }) => {

  const handleTodoClick = () => {
    toggleTodo(todo.id);
  };

  
  return (
    <div>
        <label>
            <input type="checkbox" checked={todo.completed} 
            readOnly onChange={handleTodoClick}/>

        </label>
        {todo.name}
    </div>
  )
}

export default Todo
```

続いて、残りのタスクの数が表示されるような機能を実装していく。
App.jsの中のJSXの下の方、「残りのタスク:1」としているところの1を削除して以下のようなコードに変更する。

    <div>残りのタスク:{todos.filter((todo) => !todo.completed).length}

filter関数を用いて、todo.completedの要素がfalseであるものだけを取り出して、それの要素の数を出力できるようにlengthを用いることで残りのタスクの数を表示できるようにしている。

また、例のために表示していたデフォルトの”todo1"という初期値は削除したいので”useState"の初期値を空のリストにする。

    const [todos, setTodos] = useState([]);

続いて、完了したタスクの削除という機能を実装していく。
「完了したタスクの削除」というボタンが押されたらonClickで起動する関数に適当な名前を付ける。この場合、"handleDeleteTodo"とした。
そして、filter関数を使用して"todo"の要素の中からcompleted属性が”false"のものだけを取り出して”newTodos"という変数に格納する。

そして、setTodosを使用して、”newTodos"を”todos"とする。

```
const handleDeleteTodo = () => {
    const newTodos = todos.filter((todo) => !todo.completed);
    setTodos(newTodos);
  };
```

また、一応名前が空のタスクを追加しようとしているときにそれを許可しない使用にする。
タスクを追加するときに起動する関数として作成した"handleAddTodo"か、それに相当する関数の中で

    if (name === "") return;

のように記述しておく。よって、以下のようになる。

```
 const handleAddTodo = () => {
    //タスクを追加する。
    const name = todoNameRef.current.value;
    if (name === "") return;
    setTodos((prevTodos) => {
      return [...prevTodos, {id: uuidv4(), name:name, completed: false}];
    });
    todoNameRef.current.value = null;
  };
```

name属性が空だったら、returnしろという記述である。