【React】カスタムhooksを作成してGraphQL JWT認証のログイン画面を作成する

# 概要
前回作成した[【Django】GraphQLのJWT認証を構築](https://qiita.com/ZuNo/items/71f78444b4412d7c5bfe)を利用してフロントエンド側にログイン画面の作成を紹介します。
画面はシンプルにユーザ名とパスワードのみとし、認証に成功した場合ローカルストレージにトークン類を保存して遷移するようにします。
入力Formに`react-hooks-form`、クライアントに`ApolloClient`、画面遷移に`react-router`を使用します。

# ライブラリバージョン
- typescript 4.0.3
- react 17.0.1
- @apollo/client 3.2.9
- graphql 15.4.0

# ライブラリインストール
```shell
$ yarn install @apollo/client graphql
```

# ソースコード
### ApolloClientの作成
`http://127.0.0.1:8000/graphql/`に接続するクライアントを作成します。

```typescript:index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';

const client = new ApolloClient({
  uri: 'http://127.0.0.1:8000/graphql/',
  cache: new InMemoryCache(),
});

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root'),
);
```

### カスタムhooksの作成
下記3つを返却する認証用hooksを作成します。

- `authentication`・・・認証が成功したかを返却する非同期関数。成功した場合はローカルストレージにトークン類を保存する。
- `loading`・・・ローディング中を示すbool値
- `error`・・・エラーの状態を示すオブジェクト

```typescript:useAuthentication.ts
import { useCallback } from 'react';
import { useMutation, gql, ApolloError } from '@apollo/client';

interface Payload {
  username: string;
  exp: number;
  origIat: number;
}

interface TokenAuth {
  tokenAuth: {
    token: string;
    payload: Payload;
    refreshToken: string;
    refreshExpiresIn: number;
  };
}

const TOKEN_AUTH = gql`
  mutation tokenAuth($username: String!, $password: String!) {
    tokenAuth(username: $username, password: $password) {
      token
      payload
      refreshToken
      refreshExpiresIn
    }
  }
`;

// 画面上の入力に使用
export interface Inputs {
  username: string;
  password: string;
}

interface Return {
  authentication: (inputs: Inputs) => Promise<boolean>;
  loading: boolean;
  error: ApolloError | undefined;
}

export const useAuthentication = (): Return => {
  const [tokenAuth, { loading, error }] = useMutation<TokenAuth, Inputs>(
    TOKEN_AUTH,
  );

  const authentication = useCallback(
    async (inputs: Inputs): Promise<boolean> => {
      try {
        const result = await tokenAuth({
          variables: {
            username: inputs.username,
            password: inputs.password,
          },
        });

        if (result.data !== null && result.data !== undefined) {
          localStorage.setItem('token', result.data.tokenAuth.token);
          localStorage.setItem(
            'refreshToken',
            result.data.tokenAuth.refreshToken,
          );
          localStorage.setItem(
            'refreshExoireIn',
            result.data.tokenAuth.refreshExpiresIn.toString(),
          );

          return true;
        }

        return false;
      } catch (autherror) {
        console.error(autherror);

        return false;
      }
    },
    [tokenAuth],
  );

  return { authentication, loading, error };
};

```

### ログイン画面の作成
ユーザ名とパスワードを入力して、認証に成功した場合に、`Account/mypage`に遷移します。
認証結果が返ってくるまでの読み込み中は、`Loading...`が表示されるようになっています。

```typescript:Login.tsx
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router-dom';
import React, { FC, useEffect } from 'react';
import { Inputs, useAuthentication } from 'hooks/useAuthentication';

const Login: FC = () => {
  const history = useHistory();

  const {
    register,
    handleSubmit,
    reset,
    errors,
    formState: { isSubmitSuccessful },
  } = useForm<Inputs, React.BaseSyntheticEvent>();

  const { authentication, loading, error } = useAuthentication();

  const onSubmit = async (input: Inputs, e?: React.BaseSyntheticEvent) => {
    if (e !== undefined) e.preventDefault();
    const canAuthenticated = await authentication(input);

    if (canAuthenticated) {
      history.push('/Account/mypage');
    } else {
      history.push('/Account/login');
    }
  };

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset({ username: '', password: '' });
    }
  }, [isSubmitSuccessful, reset]);

  if (loading) return <p>Loading...</p>;

  return (
    <div>
      {error ? <p>認証エラー</p> : null}
      <form onSubmit={handleSubmit(onSubmit)}>
        <input
          name="username"
          placeholder="username"
          ref={register({ required: true })}
        />
        {errors.username && <span>ユーザ名を入力してください。</span>}
        <input
          type="password"
          name="password"
          placeholder="password"
          ref={register({ required: true })}
        />
        {errors.password && <span>パスワードを入力してください。</span>}
        <input type="submit" />
      </form>
    </div>
  );
};

export default Login;

```

# 最後に
カスタムhooksを作成してログイン画面を作成してみました。
hooks内にロジックを収めることで、大分見通しの良いコードになったと思います。

まだまだ初歩的な事しかできていませんが、これからもReactの学習を進めていき共有していきたいと思います。

# 参考サイト
https://zenn.dev/maktub_bros/articles/4bcad345e5420a
https://www.apollographql.com/docs/react/get-started/
https://react-hook-form.com/jp/get-started