【続編】ReactHookでTodoアプリの作成方法の続き

 

この記事は下記記事の続きになります。

前回まではアプリの概要と環境設定(というってもcodesandboxですが…笑)

本記事ではアプリのソースをどんどん書いていきますよ!

以下の3ステップでTODOアプリを作成していこうと思います。

  1. TODOリストを画面に描画するロジックを作成
  2. TODOリストをそれっぽくスタイルしていく
  3. TODOを1つ新規追加するロジックを作成

TODOリストを画面に描画するロジックを作成

さて、コード書いていきましょう!

TODOリストを内部で保持しておく箱(オブジェクト)を作成

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

export default function App() {
  const [todoList, setTodoList] = useState([
    {
      id: 1,
      content: "TODOテストNo1",
      isDone: false
    },
    {
      id: 2,
      content: "TODOテストNo2",
      isDone: true
    }
  ]);
  return (
    <div className="App">
        Hello
    </div>
  );
}

ここで着目することは2点あります。

1つ目は「HookのuseState」を使用していることです。

useStateを使用することにより初期値の定義とそのセッターメソッドを一気に定義することができます。

また、このstateが更新されることでコンポーネント自体が再レンダーされるため、新規でTODOが追加された時に表示を更新させることができます。

2つ目は「初期値をオブジェクトIN配列」の形で初期値を定義していることです。

配列形式でTODOのみを保持しても良かったのですが、実装したい1機能にてTODOの完了/未完了の状態を保持する必要があったので、似たような変数はオブジェクトとして1つで保持している次第です。

オブジェクトIN配列という名前は以下の記事で私が勝手につけました(笑)

すごい便利なので何か名前あればいいんですけどね…

定義されたTODOを画面に表示

TODOを画面に表示する用のコンポーネントを作成します。

フォルダ構成は以下のように変更します。

ItemList.js

import {
  List,
  ListItem,
  ListItemText
} from "@material-ui/core";

const ItemList = (props) => {
  return (
    <List>
      {props.todoList.map((item) => {
          return (
            <ListItem
              key={item.id}
              role={undefined}
              dense
              button
            >
              <ListItemText id={item.id} primary={item.content} />
            </ListItem>
          );
      })}
    </List>
  );
};

export default ItemList;

App.js

import { useState } from "react";
import ItemList from "./components/ItemList";
import "./styles.css";

export default function App() {
  const [todoList, setTodoList] = useState([
    {
      id: 1,
      content: "TODOテストNo1",
      isDone: false
    },
    {
      id: 2,
      content: "TODOテストNo2",
      isDone: true
    }
  ]);
  return (
    <div className="App">
      <ItemList todoList={todoList} />
    </div>
  );
}

ここで着目する点は2つあります。

1つ目はファイル構成を変更したことです。

Reactはコンポーネント指向ですので、小さな機能ごとにコンポーネントを作成しておくことが望まれます。(こんなこと言って私もまだまだですが…)

今回は分かりやすく「components」というフォルダを作成して、その中に追加のファイルを格納しました。

2つ目は親コンポーネントで保持していたstateを表示していることです。

親コンポーネントであるAppはTODOをStateにて保持しており、それを子コンポーネントであるItemListに引数として渡します。ItemListではそれをPropsにより受け取りstateを表示している様子が伺えると思います。

表示する際はjavascriptのmapメソッドを使用して表示しています。

TODOリストをそれっぽくスタイルしていく

チェックボックスを追加したり、終わったTODOが分かりやすいようにスタイルを付けてみたりしていきます。

チェックボックスの追加

ItemList.js

import {
  Checkbox,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  makeStyles
} from "@material-ui/core";

const useStyles = makeStyles((theme) => ({
  root: {
    width: "100%",
    maxWidth: 360
  },
}));

const ItemList = (props) => {
  const classes = useStyles();
  const handleCheckbox = (id) => {
    let updatedList = props.todoList.map((item) => {
      if (item.id == id) {
        return { ...item, isDone: !item.isDone };
      }
      return item;
    });

    props.setTodoList(updatedList);
  };

  return (
    <List className={classes.root}>
      {props.todoList.map((item) => {
          return (
            <ListItem
              key={item.id}
              role={undefined}
              dense
              button
              onClick={() => handleCheckbox(item.id)}
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={item.isDone}
                  tabIndex={-1}
                  inputProps={{ "aria-labelledby": item.id }}
                />
              </ListItemIcon>
              <ListItemText id={item.id} primary={item.content} />
            </ListItem>
          );
      })}
    </List>
  );
};

export default ItemList;

App.js

import { useState } from "react";
import ItemList from "./components/ItemList";
import "./styles.css";

export default function App() {
  const [todoList, setTodoList] = useState([
    {
      id: 1,
      content: "TODOテストNo1",
      isDone: false
    },
    {
      id: 2,
      content: "TODOテストNo2",
      isDone: true
    }
  ]);
  return (
    <div className="App">
      <ItemList todoList={todoList} setTodoList={setTodoList} />
    </div>
  );
}

ここで着目する点は1つあります。

チェックボックス押下時の動作です。

チェックボックスにチェックが入るかどうかは、Checkboxタグのchecked属性で管理しています。

その中には現在isDoneフィールドの値が代入されています。

ですので、チェックボックス押下時は対象のTODOのisDoneの値を反転させてあげる必要があります。

ここでTODOオブジェクトはApp.jsのuseStateにて持っていましたので、そのセッターメソッド(setTodo)もApp.jsからpropsで引き継いであげる必要があります。

オブジェクトIN配列の値を変更する方法はjavascript特有の記法である「…」を使ってあげることで楽に変更することができます。

詳しくは以下をご参照ください。

完了/未完了でスタイルを変えてあげる

ItemList.js

import {
  Checkbox,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  makeStyles
} from "@material-ui/core";

const useStyles = makeStyles((theme) => ({
  root: {
    width: "100%",
    maxWidth: 360
  },
  finishedTodo: {
    color: "#808080"
  }
}));

const ItemList = (props) => {
  const classes = useStyles();
  const handleCheckbox = (id) => {
    let updatedList = props.todoList.map((item) => {
      if (item.id == id) {
        return { ...item, isDone: !item.isDone };
      }
      return item;
    });

    props.setTodoList(updatedList);
  };

  return (
    <List className={classes.root}>
      {/* Not finished TODO */}
      {props.todoList.map((item) => {
        if (!item.isDone) {
          return (
            <ListItem
              key={item.id}
              role={undefined}
              dense
              button
              onClick={() => handleCheckbox(item.id)}
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={item.isDone}
                  tabIndex={-1}
                  inputProps={{ "aria-labelledby": item.id }}
                />
              </ListItemIcon>
              <ListItemText id={item.id} primary={item.content} />
            </ListItem>
          );
        }
      })}
      {/* finished TODO */}
      {props.todoList.map((item) => {
        if (item.isDone) {
          return (
            <ListItem
              key={item.id}
              role={undefined}
              dense
              button
              onClick={() => handleCheckbox(item.id)}
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={item.isDone}
                  tabIndex={-1}
                  inputProps={{ "aria-labelledby": item.id }}
                />
              </ListItemIcon>
              <ListItemText
                id={item.id}
                primary={item.content}
                className={classes.finishedTodo}
              />
            </ListItem>
          );
        }
      })}
    </List>
  );
};

export default ItemList;

完了のTODO(つまりisDoneがtrue)を下に、未完了のTODO(つまりisDoneがfalse)を上に表示できるようにしています。

また、完了の方にはStyleを適用させ薄暗く表示させるようにしています。

TODOを1つ新規追加するロジックを作成

新規に追加するTODOを入力するフォームが1つ必要になってきます。

今回もコンポーネント指向に従って、新しくファイルを作成します。

ファイル構成を以下のように変更します。(CreateItem.jsをcomponentフォルダに追加しました)

新しく作成したファイルに以下のコードを記述していきます。

CreateItem.js

import { IconButton, TextField } from "@material-ui/core";
import { AssignmentTurnedIn } from "@material-ui/icons";
import { useState } from "react";

const CreateItem = (props) => {
  const [newTodo, setNewTodo] = useState("");

  const AddButton = () => {
    const addButtonClick = () => {
      props.setTodoList([
        ...props.todoList,
        {
          id: props.todoList.length + 1,
          content: newTodo,
          isDone: false
        }
      ]);
      setNewTodo("");
    };
    return (
      <IconButton>
        <AssignmentTurnedIn onClick={() => addButtonClick()} />
      </IconButton>
    );
  };
  return (
    <TextField
      label="TODOを追加"
      value={newTodo}
      onChange={(e) => setNewTodo(e.target.value)}
      InputProps={{ endAdornment: <AddButton /> }}
    />
  );
};

export default CreateItem;

App.js

import { useState } from "react";
import CreateItem from "./components/CreateItem";
import ItemList from "./components/ItemList";
import "./styles.css";

export default function App() {
  const [todoList, setTodoList] = useState([
    {
      id: 1,
      content: "TODOテストNo1",
      isDone: false
    },
    {
      id: 2,
      content: "TODOテストNo2",
      isDone: true
    }
  ]);
  return (
    <div className="App">
      <CreateItem setTodoList={setTodoList} todoList={todoList} />
      <ItemList todoList={todoList} setTodoList={setTodoList} />
    </div>
  );
}

ここで着目する点は2つあります。

1つ目はテキスト入力の値をnewTodoというuseStateにて管理している所です。

TextFieldのvalue属性とnewTodoとを同期取らせています。

また、onChange属性にてnewTodoのセッターメソッドを定義することで、入力が変わるたびに表示も変わり、useStateの値も変わるというわけです。

2つ目はstateを更新する時のロジックです。

次に「新規登録」ボタンを押下した時はnewTodoの値を親コンポーネントから取得したtodoListのセッターメソッドに追加するようにしています。

ここの辺が2つのuseStateが絡んでくるので少しややこしいですね。

最後に

レスポンシブを意識させたり、編集削除機能を作ってみたり、DBと接続してみたり、スタイルにこだわってみたりとTodoアプリ一つで開発の幅は無限にあると思います。

コメント

タイトルとURLをコピーしました