2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React】React Queryとカスタムフックを用いたログインの実装

Posted at

はじめに

ログイン画面の実装に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())します。

updateUserclearUserでは、setQueryDataを使って、それぞれキャッシュの更新と削除処理を行います。

useUser.ts
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 };
}

ローカルストレージのデータ取得/保存/削除処理は以下のようになっています。

index.ts
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()内で呼び出し、updateUserclearUserを使用します。
authServerCall内でログイン処理を行っており(サインアップ処理は未実装)、サーバからtokenを取得できればupdateUser()でクエリキャッシュ(およびローカルストレージ)も更新します。

useAuth.ts
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,
  };
}

getJWTHeaderAxiosRequestConfigの中身は以下のようになっています。

index.ts
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);

参考資料

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?