下の画像は、Redux公式ページのRedux Application Data FlowにあるReduxを使ったReactアプリのstate更新を現したアニメーションGIFです。
- Reactコンポーネントのボタンが押され、イベントハンドラーが起動
- イベントハンドラー内でReduxのActionがDispatchされる
- Redux内のReducerがstateを更新される
- 対応するReactコンポーネントが再描画され画面に表示されているstateが更新される
そして、重要な事はDispatchやReactコンポーネントが再描画は非同期に行われます。
今回の説明コード
今回のコードは、ボタンを押すとバックエンドから取得した日時を表示します。ただしバックエンドのAPIは
- 日時データ取得 (getTimestamp): 日時データ(timestamp)を取得。
GET /timestamp
- 最新日時に更新 (updateTimestamp) : 日時データを最新日時に更新、ただしこのAPIはレスポンスを返すまでに時間がかかります。
PUT /now
バックエンドのコード
import express, { Request, Response, Express } from "express";
const app: Express = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.listen(8080, () => {
console.log("Start on port 8080.");
});
let now = new Date();
app.get("/timestamp", (_req: Request, res: Response) => {
res.json({ timestamp: now.toISOString() });
});
app.put("/now", (_req: Request, res: Response) => {
setTimeout(() => {
now = new Date();
res.json({ status: "ok" });
}, 3000);
});
うっかりコード
フロントエンドのコード(React)
ボタンが押されると、最新日時に更新APIと日時データ取得APIをdispatchしていますが、dispatchは非同期処理なので、2つのAPIはほぼ同時に実行されます。ただし最新日時に更新APIは遅いので、日時データ取得APIが先に終了してしまい過去の日時が表示されてしまいます。
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch, getTimestamp, updateTimestamp } from "./store";
function App() {
const dispatch = useDispatch<AppDispatch>();
const now = useSelector((state: RootState) => state.now);
const loading = useSelector((state: RootState) => state.loading);
return (
<>
<p>日時: {now}</p>
<button
onClick={() => {
dispatch(updateTimestamp());
dispatch(getTimestamp());
}}
>
更新
</button>
{loading && <p>loading...</p>}
</>
);
}
export default App;
フロントエンドのコード(Redux Toolkit)
import { configureStore, createAsyncThunk, createSlice} from "@reduxjs/toolkit";
type TimestampState = {
now: string;
loading: boolean;
};
const initialState: TimestampState = {
now: "",
loading: false
};
const slice = createSlice({
name: "timestamp",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getTimestamp.pending, (state, _action) => {
state.now = "";
state.loading = true;
});
builder.addCase(getTimestamp.fulfilled, (state, action) => {
state.now = action.payload;
state.loading = false;
});
builder.addCase(getTimestamp.rejected, (state, action) => {
console.log("Error :", action.payload);
state.loading = false;
});
builder.addCase(updateTimestamp.pending, (state, _action) => {
state.loading = true;
});
builder.addCase(updateTimestamp.fulfilled, (state, _action) => {
state.loading = false;
});
builder.addCase(updateTimestamp.rejected, (state, action) => {
console.log("Error :", action.payload);
state.loading = false;
});
}
});
export const getTimestamp = createAsyncThunk<string, void>(
"timestamp/get",
async (_args, { rejectWithValue }) => {
try {
const res = await fetch("/timestamp");
const ts = await res.json();
console.log("=== get ", ts);
return ts.timestamp;
} catch (error: any) {
return rejectWithValue(error);
}
}
);
export const updateTimestamp = createAsyncThunk<void, void>(
"timestamp/update",
async (_args, { rejectWithValue }) => {
try {
await fetch("/now", { method: "PUT" });
console.log("=== put ");
return;
} catch (error: any) {
return rejectWithValue(error);
}
}
);
const store = configureStore({
reducer: slice.reducer,
});
export default store;
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
更新を待つコード
リファクタリングのお仕事をしてみた(2)の後半に書いたように、更新済み状態をstateに持つてば正しく動作します。しかし面倒ですね。
フロントエンドのコード(React)
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch, getTimestamp, updateTimestamp } from "./store";
function App() {
const dispatch = useDispatch<AppDispatch>();
const now = useSelector((state: RootState) => state.now);
const loading = useSelector((state: RootState) => state.loading);
const updated = useSelector((state: RootState) => state.updated);
useEffect(() => {
if (updated) {
dispatch(getTimestamp());
}
}, [updated])
return (
<>
<p>日時: {now}</p>
<button
onClick={() => {
dispatch(updateTimestamp());
}}
>
更新
</button>
{loading && <p>loading...</p>}
</>
);
}
export default App;
フロントエンドのコード(Redux Toolkit)
stateに更新済み状態updatedを追加しました、コードは変更部分のみです。
・・・
type TimestampState = {
now: string;
loading: boolean;
updated: boolean;
};
const initialState: TimestampState = {
now: "",
loading: false,
updated: false
};
const slice = createSlice({
name: "timestamp",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getTimestamp.pending, (state, _action) => {
state.now = "";
state.loading = true;
});
builder.addCase(getTimestamp.fulfilled, (state, action) => {
state.now = action.payload;
state.loading = false;
state.updated = false;
});
builder.addCase(getTimestamp.rejected, (state, action) => {
console.log("Error :", action.payload);
state.loading = false;
});
builder.addCase(updateTimestamp.pending, (state, _action) => {
state.loading = true;
state.updated = false;
});
builder.addCase(updateTimestamp.fulfilled, (state, _action) => {
state.loading = false;
state.updated = true;
});
builder.addCase(updateTimestamp.rejected, (state, action) => {
console.log("Error :", action.payload);
state.loading = false;
state.updated = false;
});
}
});
・・・
dispatchはawait出来るぞ!
よくよく調べると非同期通信を行うようなThunx系のActionはawaitで待つ事ができます!
ボタンのイベントハンドラー内で最新日時に更新APIをawaitで実行を待ってから、日時データ取得APIが実行されます。
import { useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch, getTimestamp, updateTimestamp } from "./store";
function App() {
const dispatch = useDispatch<AppDispatch>();
const now = useSelector((state: RootState) => state.now);
const loading = useSelector((state: RootState) => state.loading);
return (
<>
<p>日時: {now}</p>
<button
onClick={async () => {
await dispatch(updateTimestamp());
dispatch(getTimestamp());
}}
>
更新
</button>
{loading && <p>loading...</p>}
</>
);
}
export default App;
Redux Toolkitはうっかりコードと同じです。簡単ですね😊