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

今回はReduxを使ってMy Togo Listのデータをあつかっていきたいと思います。 (最後にはReduxのStoreからデータを取得して、MyTogoListのコンポーネントに表示できるようにします。)

ChromeにRedux DevToolsという拡張機能があるので、これをChromeにいれておくと便利です。 スクリーンショット 2022-08-19 22.21.32.png

ChromeでF12を押すとStoreの状態が視覚的にわかるというツールです。 スクリーンショット 2022-08-25 20.23.59.png


今回のアプリケーションでは、Redux Toolkitというライブラリーを使います。

Redux Toolkitは、Reduxをあつかいやすくするためのライブラリーで、シンプルに記述できるようになっています。(createSliceを使ってreducerを作ればaction typeとaction creatorをつくってくれるみたいです。) Redux Toolkitを使わない場合は、次のように書いていました。(多分こんな感じだと思います。。。。)

// action type
const GET_PRODUCTS = 'GET_PRODUCTS';

// action creator
export const getProductsAction = (products) => {
    return {
        type: GET_PRODUCTS,
        payload: products,
    }
}

// reducer
export const ProductsReducer = (state = { list: {} }, action) => {
    switch (action.type) {
        case GET_PRODUCTS:
            return {
                ...state,
                list: action.payload
            }
        default:
            return state;
    }
}

Redux Toolkitを使うと次のように書くことができます。 記述量が少なくなり、コードの見通しもよくなります。

export const productsSlice = createSlice({
  name: 'products',
  initialState: { list: [] },
  reducers: {
    getProducts(state, action) {
      state.list = action.payload;
    },
  },
});

export const { getProducts } = productsSlice.actions;

また、非同期処理が行えるReduxThunkをインストールする必要はありませんし、Redux DevToolsを使うための設定も行う必要がなくなります。

1.react-reduxのインストール

環境構築時にreact-reduxをインストールするのを忘れていましたので、ここでインストールしておきます。

npm install react-redux --save
npm install @types/react-redux --save-dev

2.Storeの作成

frontend/app/src/redux/store.tsを次のようにつくります。

import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {},
});

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

3.Appコンポーネントへの設定

AppコンポーネントをProviderというものを使ってでラッピングします。 Providerはreact-reduxからimportします。 2で作成したstoreをimportしてstoreをpropsとしてProviderコンポーネントに渡すことですべてのコンポーネントからStoreに接続することができるようになります。

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

import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './redux/store';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <Provider store={store}>
    <App />
  </Provider>
);

4.createSliceを使ってReducerなどを定義する

ステート(状態)を更新する関数や初期値などを定義していきます。 frontned/app/src/redux/togoSlice.tsを次のように作成します。

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { Togo } from '../types/togo';

// togoのステート(状態)の型を定義する
type TogoState = {
  togoList: Togo[];
};

// togoのステート(状態)の初期状態を定義する
export const initialState: TogoState = {
  togoList: [],
};

// createSliceを使用して、reducerを記述する
export const togoSlice = createSlice({
  name: 'togo',
  initialState,
  reducers: {
    getTogoList(state: TogoState, action: PayloadAction<Togo[]>) {
      state.togoList = action.payload;
    },
  },
});

// actionをエクスポートしておく
export const { getTogoList } = togoSlice.actions;

ここでは、ステートにtogoListを登録するreducerなどをつくっています。

5.Storeにreducerを登録する

4でつくったreducerをstoreで読み込んで、登録しておきます。 これで、togoステートにどのコンポーネントからでも接続できるようになります。 frontend/app/src/redux/store.ts

import { configureStore } from '@reduxjs/toolkit';

// ここから追加
import { togoSlice } from './togoSlice';
// ここまで追加

export const store = configureStore({
  reducer: {
    // ここから追加
    togo: togoSlice.reducer,
    // ここまで追加
  },
});

ChromeのRedux DevToolsでステート(状態)をみてみるとtogoListというステートがつくられ初期値として、空の配列が入っていると思います。 スクリーンショット 2022-08-25 21.11.47.png

6.サンプルデータ(sampleTogoList)をStoreに登録する

MyTogoListのコンポーネントでサンプルデータ(sampleTogoList)をStoreに登録します。 frontend/app/src/components/togo/MyTogoList.tsxを次のように修正します。

// ここから追加
import { useEffect } from 'react';
import { useDispatch } 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 { AppDispatch } from '../../redux/store';
import { getTogoList } from '../../redux/togoSlice';
import sampleTogoList from '../../sampleData/togo';
// ここまで追加

// 末尾の「(」を「{」に修正
const MyTogoList = () => {

  // ここから追加
  const dispatch: AppDispatch = useDispatch();

  useEffect(() => {
    dispatch(getTogoList(sampleTogoList));
  }, [dispatch]);
  // ここまで追加

  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>
          {sampleTogoList &&
            sampleTogoList.map((item) => (
              <TableRow key={item.id}>
                <TableCell>
                  <Checkbox checked={item.done} />
                </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' }} />
                </TableCell>
              </TableRow>
            ))}
        </TableBody>
      </Table>
      <Fab
        color="primary"
        aria-label="add"
        size="medium"
        sx={{ position: 'absolute', top: 16, right: 16 }}
      >
        <AddIcon />
      </Fab>
    </>
  );

// 「)」を「}」に修正
};

export default MyTogoList;

useDispatchでアクションを呼び出して、それに応じたreducerによって、ステートが書き換えられます。 今回の場合ですと、getTogoListというアクションが呼び出され、getTogoListというreducerによって、togoListのステートが書き換えられ、togoList(ステート)にサンプルデータが入ったという感じになります。

再度、ChromeのRedux DevToolsでステート(状態)をみてみるとtogoListにサンプルデータが入っていると思います。 スクリーンショット 2022-08-25 21.26.28.png

7.Storeからステートを取得する

現段階では、sampleTogoListをMyTogoListコンポーネントで直接表示していますので、これをStoreに登録されているtogoListを取得してきて、MyTogoListコンポーネントで表示できるようにしていきます。

frontend/app/src/components/togo/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, 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]);

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

export default MyTogoList;

useSelectorを使ってStoreにあるtogoのtogoListの値を取得してきて、mapで一行一行表示しています。

これで、今回の目標まで到達しました。

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