1. 概要
前回はReduxを使いコンポネント間で値を共有する内容についてでした。今回はRedux Thunksを使いAPI呼び出しの非同期処理についてです。
対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。
2. nodeのインストール
こちらを参考
3. プロジェクトを作成
こちらを参考
4. 必要なライブラリをインストール
こちらを参考
5. ソースコード
※前回より差分のみを記載
5-1-1. src/app/redux/redux02/page.module.scss
.component {
color: blue;
& ul {
margin-left: 20px;
& li {
list-style: disc;
}
}
}
5-1-2. src/app/redux/redux02/store.ts
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import textSlice from "./text-slice";
import imageSlice from "./image-slice";
const reducers = combineReducers({
text: textSlice,
image: imageSlice,
});
const store = configureStore({
reducer: reducers,
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
5-1-3. src/app/redux/redux02/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
5-1-4. src/app/redux/redux02/text-slice.tsx
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { RootState } from "./store";
export type TextAction = {
text: string;
};
type TextState = {
text: string;
};
const initialState: TextState = {
text: "",
};
export const textSlice = createSlice({
name: "text",
initialState,
reducers: {
setText: (state, action: PayloadAction<TextAction>) => {
const { text } = action.payload;
state.text = text;
},
},
});
const changeText = (state: RootState): TextState => state.text;
export { changeText };
export const { setText } = textSlice.actions;
export default textSlice.reducer;
5-1-5. src/app/redux/redux02/text-box.tsx
import { ChangeEvent } from "react";
import { Divider } from "@mui/material";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import { useAppSelector, useAppDispatch } from "./hooks";
import { AppDispatch } from "./store";
import { setText, changeText } from "./text-slice";
const TextBox = () => {
const textObj = useAppSelector(changeText);
const dispatch: AppDispatch = useAppDispatch();
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
dispatch(setText({ text: e.target.value }));
};
return (
<Box
sx={{
width: "100%",
p: 2,
border: "1px dashed grey",
borderRadius: "20px",
"&:hover": {
backgroundColor: "pink",
opacity: [0.9, 0.8, 0.7],
},
}}
>
<TextField
fullWidth
label="Hello"
color="secondary"
onChange={handleChange}
/>
<Divider />
{textObj.text}
</Box>
);
};
export default TextBox;
5-1-6. src/app/redux/redux02/image-slice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "./store";
type ImageState = {
src: string;
status: string;
};
const initialState: ImageState = {
src: "",
status: "",
};
export const imageSlice = createSlice({
name: "image",
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(fetchImage.pending, (state) => {
state.status = "loading";
})
.addCase(fetchImage.fulfilled, (state, action) => {
state.status = "done";
const { message } = action.payload;
state.src = message;
})
.addCase(fetchImage.rejected, (state) => {
state.status = "error";
});
},
});
const fetchImage = createAsyncThunk("image/fetchImage", async () => {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
return response.json();
});
const pickImage = (state: RootState) => state.image;
export { fetchImage, pickImage };
export default imageSlice.reducer;
5-1-7. src/app/redux/redux02/image-box.tsx
import { Button, Divider } from "@mui/material";
import Box from "@mui/material/Box";
import { useAppSelector, useAppDispatch } from "./hooks";
import { fetchImage, pickImage } from "./image-slice";
const ImageBox = () => {
const dispatch = useAppDispatch();
const image = useAppSelector(pickImage);
const handleClick = () => {
dispatch(fetchImage());
};
return (
<Box
sx={{
width: "100%",
p: 2,
border: "1px dashed grey",
borderRadius: "20px",
"&:hover": {
backgroundColor: "lightskyblue",
opacity: [0.9, 0.8, 0.7],
},
}}
>
<Button
variant="contained"
size="medium"
color="secondary"
onClick={() => handleClick()}
>
Dog Image Fetch (Random)
</Button>
<Divider sx={{ paddingBottom: 2 }} />
{image.status === "done" ? <img src={image.src} /> : image.status}
</Box>
);
};
export default ImageBox;
5-1-8. src/app/redux/redux02/child.tsx
import { Stack, Divider } from "@mui/material";
import scss from "./page.module.scss";
import GoBack from "@/lib/components/go-back";
import TextBox from "./text-box";
import ImageBox from "./image-box";
const Child = () => {
return (
<div className={scss.component}>
<GoBack />
<br />
<br />
<ul>
<li>Updating the screen</li>
<ul>
<li>createAsyncThunk</li>
</ul>
</ul>
<br />
<Stack spacing={1} sx={{ width: "50%" }}>
<TextBox />
<Divider />
<ImageBox />
</Stack>
</div>
);
};
export default Child;
5-1-9. src/app/redux/redux02/page.tsx
"use client";
import { Provider } from "react-redux";
import store from "./store";
import Child from "./child";
const Redux02 = () => {
return (
<Provider store={store}>
<Child />
</Provider>
);
};
export default Redux02;
5-1-10. src/app/redux/page.module.scss
.components {
color: blue;
& ul {
margin-left: 20px;
& li {
list-style: disc;
}
}
}
5-1-11. src/app/redux/page.tsx
"use client";
import React from "react";
import { Link } from "@mui/material";
import scss from "./page.module.scss";
const Redux = () => {
return (
<div className={scss.components}>
<ul>
<li>
<Link href="/redux/redux01" underline="hover">
Redux01
</Link>
</li>
<li>
<Link href="/redux/redux02" underline="hover">
Redux02
</Link>
</li>
</ul>
</div>
);
};
export default Redux;
6. サーバーを起動
npm run dev
7. ブラウザで確認
- http://localhost:3000
8. ディレクトリの構造
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ ├── app
│ │ ├── components
│ │ │ ├── component01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component02
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component03
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component04
│ │ │ │ ├── checkbox-demo.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ ├── radio-demo.tsx
│ │ │ │ └── select-demo.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── events
│ │ │ ├── event01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── globals.scss
│ │ ├── hooks
│ │ │ ├── hook01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook02
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook03
│ │ │ │ ├── child.tsx
│ │ │ │ ├── counter-provider.tsx
│ │ │ │ ├── grandchild.tsx
│ │ │ │ ├── myself.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook04
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook05
│ │ │ │ ├── child.tsx
│ │ │ │ ├── counter-provider.tsx
│ │ │ │ ├── grandchild.tsx
│ │ │ │ ├── myself.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook06
│ │ │ │ ├── child.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ ├── play-provider.tsx
│ │ │ │ ├── text-box.tsx
│ │ │ │ └── video-player.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── layout.module.scss
│ │ ├── layout.tsx
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ └── redux
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ ├── redux01
│ │ │ ├── child.tsx
│ │ │ ├── counter-slice.ts
│ │ │ ├── grandchild.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── myself.tsx
│ │ │ ├── page.module.scss
│ │ │ ├── page.tsx
│ │ │ └── store.ts
│ │ └── redux02
│ │ ├── child.tsx
│ │ ├── hooks.ts
│ │ ├── image-box.tsx
│ │ ├── image-slice.ts
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ ├── store.ts
│ │ ├── text-box.tsx
│ │ └── text-slice.ts
│ ├── lib
│ │ ├── common
│ │ │ ├── definitions.ts
│ │ │ └── sidebar-links.tsx
│ │ ├── components
│ │ │ ├── alert-snackbar.tsx
│ │ │ ├── go-back.tsx
│ │ │ └── spacer.tsx
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ ├── sidebar.tsx
│ │ └── utils
│ │ └── util.ts
│ └── scss
│ └── common
│ ├── _index.scss
│ ├── _mixin.scss
│ ├── _mq.scss
│ └── _variables.scss
├── tailwind.config.ts
└── tsconfig.json
26 directories, 92 files
9. 備考
今回はRedux Thunksを使いAPI呼び出しの非同期処理についてでした。
10. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Getting Started with Redux | Redux
- Getting Started with React Redux | React Redux (react-redux.js.org)
- Material UI: React components based on Material Design (mui.com)
投稿者プロフィール
-
開発好きなシステムエンジニアです。
卓球にハマってます。