Reactから外部APIを呼び出す
バックエンドと疎通を取る実装のサンプルです。APIから受け取ったメッセージを画面表示します。
cf.
ディレクトリ構成
.
└── features
└── me
├── meApi.ts # API呼び出しの実体
├── meHooks.ts # Modalの開閉などの画面操作
├── me.module.scss # 画面装飾
├── meSlice.ts # コンポーネントの状態およびアクションの定義
├── meStyles.ts # Modal向けstyle定義
├── meThunks.ts # API呼び出しなど非同期処理を伴うロジック
├── Me.tsx # 画面コンポーネント
└── meTypes.ts # 各種構造体
サンプルコード
features/me/meApi.ts
APIの呼び出しのみを行います。
/**
* ユーザ情報取得APIのリクエストモデルです。
*/
type MeRequest = {
name: string;
};
/**
* ユーザ情報取得APIからの正常レスポンスを格納するモデルです。
*/
type MeSuccess = {
ok: true;
name: string;
age: number;
};
/**
* ユーザ情報取得APIからの異常レスポンスを格納するモデルです。
*/
type MeError = {
ok: false;
message: string;
};
/**
* ユーザ情報取得APIのレスポンスモデルです。
*/
type MeResponse = MeSuccess | MeError;
/**
* ユーザ情報取得APIを呼び出します。
*
* @param req ユーザ情報検索リクエスト
* @returns ユーザ情報
*/
export const me = async (req: MeRequest): Promise<MeResponse> => {
const url = new URL("/api/v1/me", window.location.origin);
url.search = new URLSearchParams({
name: req.name,
}).toString();
const res = await fetch(url.toString(), {
method: "GET",
});
const data = await res.json();
if (!res.ok) {
return { ok: false, message: data.message };
}
return { ok: true, name: data.name, age: data.age };
};
features/me/meTypes.ts
画面からAPIなどに渡す型定義を格納します。
/**
* ユーザ情報取得処理時の入力モデルです。
*/
export type FetchMeArgs = {
name: string;
};
/**
* ユーザ情報取得成功時の状態をactionに渡すモデルです。
*/
export type FetchMeSuccess = {
name: string;
age: number;
};
/**
* ユーザ情報取得失敗時の状態をactionに渡すモデルです。
*/
export type FetchMeError = {
message: string;
};
features/me/meThunks.ts
非同期処理をハンドリングします。。API呼び出し関数を実行し、その結果に対応した型を戻すことでslice側で状態の更新が行われます。
import { createAsyncThunk } from "@reduxjs/toolkit";
import { me } from "./meApi";
import type { FetchMeArgs, FetchMeError, FetchMeSuccess } from "./meTypes";
/**
* ユーザ情報取得APIを呼び出して取得したユーザ情報をstateに保持します。
*/
export const fetchMeThunk = createAsyncThunk<
FetchMeSuccess,
FetchMeArgs,
{ rejectValue: FetchMeError }
>("me/fetch", async (form, { rejectWithValue }) => {
try {
const res = await me({ name: form.name });
if (!res.ok) {
return rejectWithValue({ message: res.message });
}
return { name: res.name, age: res.age };
} catch {
return rejectWithValue({ message: "不明なエラーが発生しました。" });
}
});
features/me/meSlice.ts
API呼び出し時の状態管理をextraReducersで行います。
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { fetchMeThunk } from "./meThunks";
/**
* ユーザ情報表示コンポーネントの状態を保持するstateです。
*/
type MeState = {
profile: string;
loading: boolean;
errorMessage?: string;
isModalOpen: boolean;
};
const initialState: MeState = {
profile: "",
loading: false,
errorMessage: undefined,
isModalOpen: false,
};
const meSlice = createSlice({
name: "me",
initialState,
reducers: {
/**
* モーダルの開閉制御
*/
setIsModalOpen: (state: MeState, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
},
},
extraReducers: (builder) => {
builder
/**
* ユーザ取得API呼び出し開始時の状態遷移
*/
.addCase(fetchMeThunk.pending, (state) => {
state.loading = true;
})
/**
* ユーザ取得API呼び出し正常終了の状態遷移
*/
.addCase(fetchMeThunk.fulfilled, (state, action) => {
state.profile = action.payload.name + " (" + action.payload.age + ")";
state.errorMessage = "";
state.loading = false;
})
/**
* ユーザ取得API呼び出し異常終了時の状態遷移
*/
.addCase(fetchMeThunk.rejected, (state, action) => {
state.errorMessage = action.payload?.message;
state.isModalOpen = true;
state.loading = false;
});
},
});
export const { setIsModalOpen } = meSlice.actions;
export default meSlice.reducer;
features/me/meHooks.ts
ボタンクリック時など画面操作時の挙動を定義します。
import { useAppDispatch } from "../../app/hooks";
import { setIsModalOpen } from "./meSlice";
import { fetchMeThunk } from "./meThunks";
/**
* ユーザ情報取得・表示コンポーネント向けのHooksです。
*
* @returns ユーザ情報取得・表示コンポーネント向けHooks
*/
export const useMeHooks = () => {
const dispatch = useAppDispatch();
/**
* 検索ボタン押下時の動作を定義します。
*/
const handleClickSearch = async (name: string) => {
await dispatch(fetchMeThunk({ name: name }));
};
/**
* エラーメッセージモーダルクローズ時の動作を定義します。
*/
const handleRequestClose = () => {
dispatch(setIsModalOpen(false));
};
return {
handleClickSearch,
handleRequestClose,
};
};
features/me/meStyles.ts
Modal向けのstyle定義です。
import type { Styles } from "react-modal";
/**
* エラーメッセージモーダル向けのstyleです。
*/
export const modalStyles: Styles = {
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
marginRight: "-50%",
transform: "translate(-50%, -50%)",
color: "#000000",
},
};
features/me/Me.tsx
事前にreact-modalをインストールしておいてください。
npm install --save react-modal @types/react-modal
また、main.tsxに下記を追加してください。
Modal.setAppElement("#root");
stateの値を使って画面のレンダリングを行います。
import Modal from "react-modal";
import { useAppSelector } from "../../app/hooks";
import style from "./me.module.scss";
import { useMeHooks } from "./meHooks";
import { modalStyles } from "./meStyles";
/**
* ユーザ情報を取得・表示するコンポーネントです。
*
* @returns ユーザ情報表示コンポーネント
*/
export const Me = () => {
const meState = useAppSelector((state) => state.me);
const { handleClickSearch, handleRequestClose } = useMeHooks();
return (
<div className={style.container}>
<Modal
isOpen={meState.isModalOpen}
onRequestClose={handleRequestClose}
style={modalStyles}
contentLabel="Error message Modal"
>
{meState.errorMessage ?? <div>{meState.errorMessage}</div>}
</Modal>
{meState.profile ?? (
<div className={style.profile}>{meState.profile}</div>
)}
<div className={style.searchButtonWrapper}>
<button
onClick={() => handleClickSearch("nob")}
className={style.searchButton}
>
検索
</button>
</div>
</div>
);
};
features/me/me.module.scss
$fontSize: 18px;
$borderRadius: 10px;
.container {
text-align: center;
.searchButtonWrapper {
padding: 20px;
.searchButton {
border-radius: $borderRadius;
width: 120px;
height: 40px;
font-size: $fontSize;
background-color: #ff9900;
}
.searchButton:hover {
cursor: pointer;
background-color: #fa6f00;
}
}
}