前回までの記事 アプリ開発を楽しむ【#1:アプリの概要】 アプリ開発を楽しむ【#2:環境構築1(React+TypeScript)】 アプリ開発を楽しむ【#3:環境構築2 (ESLint+Prettier)】 アプリ開発を楽しむ【#4:ヘッダー】 アプリ開発を楽しむ【#5:MyTogoList】 アプリ開発を楽しむ【#6:Reduxで状態管理1】

今回は、MyTogoListの状態をユーザーの操作によって更新していきたいと思います。

具体的には、以下の2つのことをしていきます。 ①MyTogoListのチェックボックスにチェックをつけたり、外したりできるようにする。 ②削除アイコンをクリックすると、該当のtogoが削除される。

現状では、MyTogoListのチェックボックスをクリックしても、削除アイコンをクリックしても、何も起こりません。 これをクリックしたときに、ステート(状態)を変更して、画面に反映していきます。

1.チェックボックスの状態更新

MyTogoListにあるチェックボックスは、行ったらチェックする用のチェックボックスです。 togoのデータ項目は次のようになっていました。

要素 説明
id 識別子
done 行ったらチェック(True or False)
location 行きたい場所(行った場所)
position 緯度経度(lat: 緯度、lng: 経度)
tag タグ

チェックボックスをクリックしたときに、doneの項目をfalseもしくはtrueに変更するようにしていきます。

frontend/app/src/redux/togoSlice.tsを次のように修正します。

export const togoSlice = createSlice({
  name: 'togo',
  initialState,
  reducers: {
    getTogoList(state: TogoState, action: PayloadAction<Togo[]>) {
      state.togoList = action.payload;
    },
    // ここから追加
    updateTogoDone(state: TogoState, action: PayloadAction<number>) {
      state.togoList = state.togoList.map((togo) => {
        if (togo.id === action.payload) {
          togo.done = !togo.done;
        }
        return togo;
      });
    },
    // ここまで追加
  },
});

// ここから修正
export const { getTogoList, updateTogoDone } = togoSlice.actions;
// ここまで修正

updateTogoDoneというreducerとactionをつくっています。

updateTogoDone(state: TogoState, action: PayloadAction<number>) {
  state.togoList = state.togoList.map((togo) => {
    if (togo.id === action.payload) {
      togo.done = !togo.done;
    }
    return togo;
  });
},

状態を変更したいtogoのidをpayloadとして渡し、そのpayloadとtogoリストのidが同じだった場合に、doneの状態を変更しています。

frontend/app/src/components/MyTogoList.tsxを修正します。

import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
  Checkbox,
  Chip,
  Fab,
  Link,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  Typography,
} from '@mui/material';
import {
  Add as AddIcon,
  Delete as DeleteIcon,
  LocationOn as LocationOnIcon,
  ModeEditOutline as ModeEditOutlineIcon,
} from '@mui/icons-material';
import { RootState, AppDispatch } from '../../redux/store';

// ここから修正
import { getTogoList, updateTogoDone, initialState } from '../../redux/togoSlice';
// ここまで修正

import sampleTogoList from '../../sampleData/togo';

const MyTogoList = () => {
  const dispatch: AppDispatch = useDispatch();

  const { togoList } = useSelector((state: RootState) => state.togo || initialState);

  useEffect(() => {
    dispatch(getTogoList(sampleTogoList));
  }, [dispatch]);

  // ここから追加
  const handleChangeTogoDone = (id: number) => {
    dispatch(updateTogoDone(id));
  };
  // ここまで追加

  return (
    <>
      <Typography
        component="h2"
        variant="h6"
        color="primary"
        gutterBottom
        sx={{ fontWeight: 'bold' }}
      >
        My List
      </Typography>
      <Table size="small">
        <TableHead>
          <TableRow>
            <TableCell>&nbsp;</TableCell>
            <TableCell>場所</TableCell>
            <TableCell>タグ</TableCell>
            <TableCell>地図</TableCell>
            <TableCell>編集</TableCell>
            <TableCell>削除</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {togoList &&
            togoList.map((item) => (
              <TableRow key={item.id}>
                <TableCell>

                  // ここから修正
                  <Checkbox checked={item.done} onChange={() => handleChangeTogoDone(item.id)} />
                  // ここまで修正

                </TableCell>
                <TableCell>{item.location}</TableCell>
                <TableCell>
                  <Chip label={item.tag} color="primary" size="small" />
                </TableCell>
                <TableCell>
                  <Link
                    href={`https://maps.google.co.jp/maps?ll=${item.position.lat},${item.position.lng}&z=20`}
                    underline="none"
                    target="_blank"
                    rel="noopener"
                  >
                    <LocationOnIcon />
                  </Link>
                </TableCell>
                <TableCell>
                  <ModeEditOutlineIcon sx={{ cursor: 'pointer' }} />
                </TableCell>
                <TableCell>
                  <DeleteIcon sx={{ cursor: 'pointer' }} onClick={() => handleDelteTogo(item.id)} />
                </TableCell>
              </TableRow>
            ))}
        </TableBody>
      </Table>
      <Fab
        color="primary"
        aria-label="add"
        size="medium"
        sx={{ position: 'absolute', top: 16, right: 16 }}
      >
        <AddIcon />
      </Fab>
    </>
  );
};

export default MyTogoList;

これで、チェックボックスにチェックをつけたり、外したりすることができるようになりました。

2.削除時の状態更新

削除する場合も同様です。 frontend/app/src/redux/togoSlice.tsを次のように修正します。

export const togoSlice = createSlice({
  name: 'togo',
  initialState,
  reducers: {
    getTogoList(state: TogoState, action: PayloadAction<Togo[]>) {
      state.togoList = action.payload;
    },
    updateTogoDone(state: TogoState, action: PayloadAction<number>) {
      state.togoList = state.togoList.map((togo) => {
        if (togo.id === action.payload) {
          togo.done = !togo.done;
        }
        return togo;
      });
    },

    // ここから追加
    deleteTogo(state: TogoState, action: PayloadAction<number>) {
      state.togoList = state.togoList.filter((togo) => togo.id !== action.payload);
    },
    // ここまで修正

  },
});

// ここから修正
export const { getTogoList, updateTogoDone, deleteTogo } = togoSlice.actions;
// ここまで修正

状態を変更したい(ここでは削除したい)togoのidをpayloadとして渡し、そのpayloadとtogoリストのidが同じものは、togoListの配列から除外しています。

frontend/app/src/components/MyTogoList.tsxを修正します。

import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
  Checkbox,
  Chip,
  Fab,
  Link,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  Typography,
} from '@mui/material';
import {
  Add as AddIcon,
  Delete as DeleteIcon,
  LocationOn as LocationOnIcon,
  ModeEditOutline as ModeEditOutlineIcon,
} from '@mui/icons-material';
import { RootState, AppDispatch } from '../../redux/store';

// ここから修正
import { getTogoList, updateTogoDone, deleteTogo, initialState } from '../../redux/togoSlice';
// ここまで修正

import sampleTogoList from '../../sampleData/togo';

const MyTogoList = () => {
  const dispatch: AppDispatch = useDispatch();

  const { togoList } = useSelector((state: RootState) => state.togo || initialState);

  useEffect(() => {
    dispatch(getTogoList(sampleTogoList));
  }, [dispatch]);

  const handleChangeTogoDone = (id: number) => {
    dispatch(updateTogoDone(id));
  };

  // ここから追加
  const handleDeleteTogo = (id: number) => {
    dispatch(deleteTogo(id));
  };
  // ここまで追加

  return (
    <>
      <Typography
        component="h2"
        variant="h6"
        color="primary"
        gutterBottom
        sx={{ fontWeight: 'bold' }}
      >
        My List
      </Typography>
      <Table size="small">
        <TableHead>
          <TableRow>
            <TableCell>&nbsp;</TableCell>
            <TableCell>場所</TableCell>
            <TableCell>タグ</TableCell>
            <TableCell>地図</TableCell>
            <TableCell>編集</TableCell>
            <TableCell>削除</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {togoList &&
            togoList.map((item) => (
              <TableRow key={item.id}>
                <TableCell>
                  <Checkbox checked={item.done} onChange={() => handleChangeTogoDone(item.id)} />
                </TableCell>
                <TableCell>{item.location}</TableCell>
                <TableCell>
                  <Chip label={item.tag} color="primary" size="small" />
                </TableCell>
                <TableCell>
                  <Link
                    href={`https://maps.google.co.jp/maps?ll=${item.position.lat},${item.position.lng}&z=20`}
                    underline="none"
                    target="_blank"
                    rel="noopener"
                  >
                    <LocationOnIcon />
                  </Link>
                </TableCell>
                <TableCell>
                  <ModeEditOutlineIcon sx={{ cursor: 'pointer' }} />
                </TableCell>
                <TableCell>

                  // ここから修正
                  <DeleteIcon
                    sx={{ cursor: 'pointer' }}
                    onClick={() => handleDeleteTogo(item.id)}
                  />
                  // ここまで修正

                </TableCell>
              </TableRow>
            ))}
        </TableBody>
      </Table>
      <Fab
        color="primary"
        aria-label="add"
        size="medium"
        sx={{ position: 'absolute', top: 16, right: 16 }}
      >
        <AddIcon />
      </Fab>
    </>
  );
};

export default MyTogoList;

これで、削除のアイコンをクリックすると、該当のtogoが削除されるようになります。

実際には、Storeの状態を変更しているだけなので、ブラウザの更新ボタンを押すと、再度sampleTogoListが読み込まれ、最初の状態に戻ります。

実際のアプリケーションでは、ステート(状態)の更新を行う前に、データベースなどの元データを更新する必要があります。 例えば、削除した場合は、データベースと通信して、そのデータをデータベースから削除し、Storeの状態を更新する流れになると思います。

今回はここまでです。

コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_7のブランチを参照してください。 https://github.com/KINE-M/togo_app