はじめに
ログイン画面の実装にReact Queryを使ってみたので記事にまとめました。
React Query関連の別の記事も書いているので、興味があればご覧ください。
実装
今回はログインにJWT認証を使用します。
簡単に説明すると、ログインが成功したらサーバからトークンが発行されて、そのトークンをリクエストのヘッダーに埋め込むことで本人であることを確認するといった認証方式です。
実装にあたり、以下を呼び出す2つのカスタムフックを作成します。
-
useUser()
: user / updateUser(キャッシュ更新) / clearUser(キャッシュ削除) -
useAuth()
: signin / signup / signout
useUser()
useUser()
では、クエリキャッシュとローカルストレージのデータ管理を行います。
useQuery()
のinitialData
オプションにgetStoredUser
を設定することで、ローカルストレージのデータをクエリキャッシュとして保存します。
initialData
は登録済みのデータをデフォルトでフォームに表示したい場合などに使用します。
また、onSuccess
オプションでは、クエリによるデータ取得が成功したときやsetQueryData
でキャッシュが更新されたときに、関数(処理)を発火させることができます。
今回のケースでは、データがnull(ユーザが未ログイン)の場合にはローカルストレージのデータも削除(clearStoreUser()
)して、それ以外の場合にはローカルストレージにデータを登録(setStoredUser()
)します。
updateUser
やclearUser
では、setQueryData
を使って、それぞれキャッシュの更新と削除処理を行います。
import { AxiosResponse } from 'axios';
import { useQuery, useQueryClient } from 'react-query';
import type { User } from '../../../../../shared/types';
import { axiosInstance, getJWTHeader } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
import {
clearStoredUser,
getStoredUser,
setStoredUser,
} from '../../../user-storage';
async function getUser(user: User | null): Promise<User | null> {
if (!user) return null;
const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get(
`/user/${user.id}`,
{
headers: getJWTHeader(user),
},
);
return data.user;
}
interface UseUser {
user: User | null;
updateUser: (user: User) => void;
clearUser: () => void;
}
export function useUser(): UseUser {
const queryClient = useQueryClient();
const { data: user } = useQuery(queryKeys.user, () => getUser(user), {
initialData: getStoredUser,
onSuccess: (received: User | null) => {
if (!received) {
clearStoredUser();
} else {
setStoredUser(received);
}
},
});
function updateUser(newUser: User): void {
queryClient.setQueryData(queryKeys.user, newUser);
}
function clearUser() {
queryClient.setQueryData(queryKeys.user, null);
queryClient.removeQueries('user-appointments');
}
return { user, updateUser, clearUser };
}
ローカルストレージのデータ取得/保存/削除処理は以下のようになっています。
import { User } from '../../../shared/types';
const USER_LOCALSTORAGE_KEY = 'lazyday_user';
export function getStoredUser(): User | null {
const storedUser = localStorage.getItem(USER_LOCALSTORAGE_KEY);
return storedUser ? JSON.parse(storedUser) : null;
}
export function setStoredUser(user: User): void {
localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(user));
}
export function clearStoredUser(): void {
localStorage.removeItem(USER_LOCALSTORAGE_KEY);
}
useAuth()
作成したuserUser()
をuserAuth()
内で呼び出し、updateUser
とclearUser
を使用します。
authServerCall
内でログイン処理を行っており(サインアップ処理は未実装)、サーバからtoken
を取得できればupdateUser()
でクエリキャッシュ(およびローカルストレージ)も更新します。
import { axiosInstance } from '../axiosInstance';
import { useCustomToast } from '../components/app/hooks/useCustomToast';
import { useUser } from '../components/user/hooks/useUser';
interface UseAuth {
signin: (email: string, password: string) => Promise<void>;
signup: (email: string, password: string) => Promise<void>;
signout: () => void;
}
export function useAuth(): UseAuth {
const SERVER_ERROR = 'There was an error contacting the server.';
const toast = useCustomToast();
const { clearUser, updateUser } = useUser();
async function authServerCall(
urlEndpoint: string,
email: string,
password: string,
): Promise<void> {
try {
const { data, status } = await axiosInstance({
url: urlEndpoint,
method: 'POST',
data: { email, password },
headers: { 'Content-Type': 'application/json' },
});
if (status === 400) {
toast({ title: data.message, status: 'warning' });
return;
}
if (data?.user?.token) {
toast({
title: `Logged in as ${data.user.email}`,
status: 'info',
});
updateUser(data.user);
}
} catch (errorResponse) {
toast({
title: errorResponse?.response?.data?.message || SERVER_ERROR,
status: 'error',
});
}
}
async function signin(email: string, password: string): Promise<void> {
authServerCall('/signin', email, password);
}
async function signup(email: string, password: string): Promise<void> {
authServerCall('/user', email, password);
}
function signout(): void {
clearUser();
toast({
title: 'Logged out!',
status: 'info',
});
}
return {
signin,
signup,
signout,
};
}
getJWTHeader
やAxiosRequestConfig
の中身は以下のようになっています。
interface jwtHeader {
Authorization?: string;
}
export function getJWTHeader(user: User): jwtHeader {
return { Authorization: `Bearer ${user.token}` };
}
const config: AxiosRequestConfig = { baseURL: baseUrl };
export const axiosInstance = axios.create(config);
参考資料