目次
今回ローディング機能を実装するアプリケーションの仕様について
今回はToDoリスト管理アプリを例に、ローディング機能の実装方法を紹介します。
当該アプリケーションは、非同期処理の実行状況に応じてActionの発行とRedux Store内のState(以下State)を更新する実装になっています。
たとえばToDo一覧を非同期で取得するgetTodos
という処理は、非同期処理の実行状況に応じて以下のActionを発行します。
- 実行中(pending)の時: “getTodos/pending”
- 成功(fulfilled)の時: “getTodos/fulfilled”
- 失敗(rejected)の時: “getTodos/rejected”
ActionTypeとStateの対応は以下の通りです。
// ActionTypeが『getTodos/pending』の時
{
entities: {
todos: {
allIds: [],
byId: {}
}
},
requests: {
getTodos: {
status: 'REQUEST'
}
}
}
// ActionTypeが『getTodos/fulfilled』の時
{
entities: {
todos: {
allIds: [...],
byId: {...}
}
},
requests: {
getTodos: {
status: 'SUCCESS'
}
}
}
// ActionTypeが『getTodos/rejected』の時
{
entities: {
todos: {
allIds: [],
byId: {}
}
},
requests: {
getTodos: {
status: 'FAILURE'
}
}
}
上記のState実装の詳細解説はRedux Toolkitを利用して非同期処理のステータスをStateで管理するで紹介しています。
今回実現すること
ToDoリスト一覧画面を表示する際、ToDoの取得が完了するまでの間ローディング状態を表示させるようにします。
今回紹介する実装パターンについて
今回は『Selector経由でStateを取得し、Stateの値に応じてコンポーネントを出し分ける』という基本方針のもと、以下の3つの実装パターンについて紹介します。
- 実行中と失敗はローディング状態にする
- 実行中はローディング状態、失敗は失敗画面を表示する
- 実行中はローディング状態、実行完了(失敗もしくは成功)で画面を表示する
以下では各パターンの実装を紹介します。
なお、ローディング中に表示するアイコンはsvg-spinners/svg-css/180-ring.svgを利用しています。
パターン1: 実行中と失敗はローディング状態にする
Selector経由で非同期処理の実行状況に関するStateを取得します。
そして、成功の場合はToDo一覧を表示、そうでない場合(つまり実行中と失敗の場合)はローディング状態を表示するようにします。
selector.ts
import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "../store";
import { RequestState } from "../types/state/requests";
const selectRequests = (state: AppState): RequestState => state.requests;
const selectRequest = (actionType: string) =>
createSelector([selectRequests], (requests) => requests[actionType] || {});
// 非同期処理の実行状況を取得するSelector
export const selectRequestStatus = (actionType: string) =>
createSelector([selectRequest(actionType)], (request) => request?.status);
TodoListContainer.tsx
import { FC, useEffect } from "react";
import { TodoEntity } from "../types/state/todos";
import { selectTodosByVisibilityFilter } from "../selectors/todo";
import { getTodos } from "../reducers/todosSlice";
import { useSelector } from "react-redux";
import { useAppDispatch } from "../lib/hooks/useAppDispatch";
import { TodoList } from "./TodoList";
import { selectRequestStatus } from "../selectors/requests";
import { Loading } from "./shared/Loading";
import { RequestStatus } from "../types/constants/requestStatusType";
export const TodoListContainer: FC = () => {
const dispatch = useAppDispatch();
const todos: TodoEntity[] = useSelector(selectTodosByVisibilityFilter);
const requestStatus = useSelector(selectRequestStatus(getTodos.typePrefix));
useEffect(() => {
dispatch(getTodos());
}, [dispatch]);
return requestStatus === RequestStatus.Success ? (
// ToDo一覧を表示するコンポーネント
<TodoList todos={todos} />
) : (
// ローディングアイコンを表示するコンポーネント
<Loading />
);
};
パターン2: 実行中はローディング状態、失敗は失敗画面を表示する
先ほど紹介したパターン1に失敗のケースを追加することで当該パターンを実装できます。
TodoListContainer.tsx
+ import { Failure } from "./shared/Failure";
(略)
export const TodoListContainer: FC = () => {
const dispatch = useAppDispatch();
const todos: TodoEntity[] = useSelector(selectTodosByVisibilityFilter);
const requestStatus = useSelector(selectRequestStatus(getTodos.typePrefix));
useEffect(() => {
dispatch(getTodos());
}, [dispatch]);
// 失敗画面を表示するコンポーネント
+ if (requestStatus === RequestStatus.Failure) return <Failure />;
return requestStatus === RequestStatus.Success ? (
<TodoList todos={todos} />
) : (
<Loading />
);
};
パターン3: 実行中はローディング状態、実行完了(失敗もしくは成功)で画面を表示する
パターン1・パターン2のようにコンポーネント上で非同期処理の実行状況の判定をしてもよいですが、実行完了の判定をSelectorに実装することでコンポーネントのロジックが簡潔になります。
selector.ts
import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "../store";
import { RequestStatus } from "../types/constants/requestStatusType";
import { RequestState } from "../types/state/requests";
const selectRequests = (state: AppState): RequestState => state.requests;
const selectRequest = (actionType: string) =>
createSelector([selectRequests], (requests) => requests[actionType] || {});
// 実行完了(成功もしくは失敗)の場合trueを返すSelector
export const selectHasRequestDone = (actionType: string) =>
createSelector([selectRequest(actionType)], (request) => {
const complatedRequestStatuses = [
RequestStatus.Failure,
RequestStatus.Success,
];
return complatedRequestStatuses.some((status) =>
[request.status].includes(status)
);
});
TodoListContainer.tsx
import { FC, useEffect } from "react";
import { TodoEntity } from "../types/state/todos";
import { selectTodosByVisibilityFilter } from "../selectors/todo";
import { getTodos } from "../reducers/todosSlice";
import { useSelector } from "react-redux";
import { useAppDispatch } from "../lib/hooks/useAppDispatch";
import { TodoList } from "./TodoList";
import { selectHasRequestDone } from "../selectors/requests";
import { Loading } from "./shared/Loading";
export const TodoListContainer: FC = () => {
const dispatch = useAppDispatch();
const todos: TodoEntity[] = useSelector(selectTodosByVisibilityFilter);
const hasRequestDone = useSelector(
selectHasRequestDone(getTodos.typePrefix)
);
useEffect(() => {
dispatch(getTodos());
}, [dispatch]);
// 実行が完了するとhasRequestDoneがtrueになりTodo一覧が表示される
return hasRequestDone ? <TodoList todos={todos} /> : <Loading />;
};
さいごに
今回の実装が組み込まれたサンプルコードはnishina555/next-thunk-todoappで公開しています。
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!