アプリ開発を楽しむ【#6:Reduxで状態管理1】
前回までの記事 アプリ開発を楽しむ【#1:アプリの概要】 アプリ開発を楽しむ【#2:環境構築1(React+TypeScript)】 アプリ開発を楽しむ【#3:環境構築2 (ESLint+Prettier)】 アプリ開発を楽しむ【#4:ヘッダー】 アプリ開発を楽しむ【#5:MyTogoList】
今回はReduxを使ってMy Togo Listのデータをあつかっていきたいと思います。 (最後にはReduxのStoreからデータを取得して、MyTogoListのコンポーネントに表示できるようにします。)
ChromeにRedux DevToolsという拡張機能があるので、これをChromeにいれておくと便利です。
ChromeでF12を押すとStoreの状態が視覚的にわかるというツールです。
今回のアプリケーションでは、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というステートがつくられ初期値として、空の配列が入っていると思います。
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> </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にサンプルデータが入っていると思います。
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> </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
ネズミ
こんな拡張機能ってあるんですね!