SPA環境におけるページ制御をreact, redux, saga, axiosで実装してみた
※Frontのみ扱います
環境
- react:16.12
- react-redux: 7.1.3
- react-router-dom: 5.1.2
API仕様
- APIリクエスト時、認証失敗した場合は401を返す
できあがり要件
- メンバー用ページは認証成功したユーザーのみ表示可能
- ログイン後に認証が切れた場合、メンバー用ページが表示できないようにする
実装
あらすじ
- ページに基本となるルーティングを作成する
- 認証ルート配下はAuthコンポーネントで認証制御
- 認証状態はredux, redux-sagaを利用して管理
- requestモジュールから401を検知してreact-routerで遷移
ディレクトリ構成
user
├── api.ts //api通信用
├── index.ts
├── redux.ts // いわゆるAction,Reducer等
└── saga.ts // Saga
ルーティング
router.tsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const AppRouter: React.FC = () => (
<Router>
<Switch>
<Route exact path='/' component={Top} />
{/* 認証ルート */}
<Route path='/member'>
<Auth>
<Switch>
<Route path="/member/signed" component={Signed} />
<Route component={NotFound} />
</Switch>
</Auth>
</Route>
<Route component={NotFound} />
</Switch>
</Router>
);
Authモジュール
- 認証状態取得前なら取得取得するまでcircular表示。認証できない場合はサインインページへ遷移
auth.tsx
const Auth = ({ user: { isSignedIn, isFetched }, fetchInfo, children }) => {
if (!isFetched) {
fetchInfo();
}
return (
<>
{isFetched ? (
<Fetched isSignedIn={isSignedIn} children={children} />
) : (
<Fetching />
)}
</>
);
};
const Fetching = () => (
<div className={styles.center}>
<Circular />
</div>
);
const Fetched = ({ isSignedIn, children }) =>
isSignedIn ? children : <Redirect to='/sign_in' />;
export default connect(mapStateToProps, mapDispatchToProps)(Auth);
redux, redux-saga関連
- 認証情報取得。認証通信がエラーの場合はSIGNED_IN = falseをkeepし、authモジュールによってページ遷移させる
redux.ts
const signedIn = () => {
return {
type: SIGNED_IN
};
};
const signedOut = () => {
return {
type: SIGNED_OUT
};
};
const signOut = () => ({ type: SIGN_OUT });
const fetched = () => ({ type: FETCHED });
const fetchInfo = payload => ({ type: FETCH_INFO, payload });
const resetState = () => ({ type: RESET_STATE });
saga.ts
function* fetchInfo() {
const isSuccess = yield call(api.fetchInfo);
if (isSuccess) {
yield all([put({ type: SIGNED_IN }), put({ type: FETCHED })]);
} else {
yield all([put({ type: FETCHED }), put({ type: SIGNED_OUT })]);
}
}
api.ts
export const fetchInfo = async () => {
let isSignedIn = true;
const requester = requestManager.get();
await requester.get(API_PATH.AUTH_INFO).catch(err => {
isSignedIn = false;
});
return isSignedIn;
};
その他通信時に認証が切れた場合のハンドリング
- axiosをwrapしたrequestモジュールを作成し、認証制御機能を組み込む
utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from "axios";
import { mapDispatchToProps as userActions } from "../modules/user";
import { AllState } from "../store";
class RequestManager {
private store?: AllState;
private requester?: AxiosInstance;
setStore(store: AllState) {
this.store = store;
}
getInstance(): AxiosInstance {
if (!this.store) {
throw new Error("store is not initialized");
}
if (!this.requester) {
this.requester = this._getBaseInstance();
}
return this.requester;
}
private _getInstance(): AxiosInstance {
const instance = axios.create({
baseURL: "http://localhost:3000",
headers: { "content-type": "application/json" },
withCredentials: true
});
instance.interceptors.response.use(response => {
const { status } = response;
if (status === 401) {
this.store.dispatch(userActions.resetState());
}
return response;
});
return instance;
}
}
export default new RequestManager();
このようにすれば、RequestManagerから生成されたaxiosインスタンス経由でリクエスト時に401の場合、isSignedIn: falseに変更され、認証ルート内の場合はサインインページへ遷移させられる