React Apollo Hooks を使ってログイン機能を作った

# はじめに

`react-apollo-hooks`が便利だったので`SPA`なログイン機能を作りながら使い方を紹介します。

実際に作成したプロジェクトは[Github](https://github.com/pure-adachi/sample-login-with-react-apollo-hooks)にあげてます。

今回もプロジェクトの雛形は `create-react-app` で作成します。

`TypeScript`で作ってみたのでコマンドは以下です。

```sh
$ create-react-app sample-login-with-react-apollo-hooks --typescript
```

※バックエンド（APIサーバ）について紹介は含んでいません。

# バージョン

| パッケージ | バージョン |
| :-- | :--: |
| typescript | 3.5.3 |
| react | 16.9.0 |
| react-apollo | 3.0.1 |
| react-apollo-hooks | 0.5.0 |
| graphql | 14.4.2 |
| graphql-tag | 2.10.1 |
| react-router-dom | 5.0.1 |

# 今回作成する機能

- ログインに成功するとルートページにリダイレクト
    - バックエンドから受け取ったトークンは、`localStorage`に格納します。
- ログインに失敗するとエラーを表示
- 既にログイン済みの場合、ルートページにリダイレクト
    - `localStorage`に格納したトークンで問い合わせ、ログイン済みか判断します。

# 画面の準備

ログインページとルートページを準備します。

```tsx:src/Login/index.tsx
import React from "react";

const Login = () => {
  return (
    <div>
      <div>
        <label>
          ID
          <br />
          <input type="text" />
        </label>
      </div>
      <div>
        <label>
          PW
          <br />
          <input type="password" />
        </label>
      </div>
      <div>
        <input type="submit" value="ログイン" />
      </div>
    </div>
  );
};

export default Login;
```

```tsx:src/Home/index.tsx
import React from "react";

const Home = () => {
  return <div>Home</div>;
};

export default Home;
```

# ルートの準備

```tsx:src/index.tsx
import React from "react";
import { Route, Switch } from "react-router-dom";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import * as serviceWorker from "./serviceWorker";
import Home from "./Home";
import Login from "./Login";

ReactDOM.render(
  <BrowserRouter>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/login" component={Login} />
    </Switch>
  </BrowserRouter>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```

## `http://localhost:3000/login`にアクセス

![スクリーンショット 2019-07-14 22.39.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/e2795944-0ffc-a12e-c922-7fb67629f78e.png)

## `http://localhost:3000/`にアクセス

![スクリーンショット 2019-07-14 22.40.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/1a46cc66-7267-e787-e142-8ab301b701bf.png)

ページとルートの準備が出来ました。

# `react-apollo`の準備

`client`を作成します。

```diff:src/index.tsx
 import React from "react";
 import ReactDOM from "react-dom";
 import { BrowserRouter } from "react-router-dom";
 import * as serviceWorker from "./serviceWorker";
+import { ApolloProvider } from "react-apollo-hooks";
+import { InMemoryCache } from "apollo-cache-inmemory";
+import { ApolloClient } from "apollo-client";
+import { setContext } from "apollo-link-context";
+import { createHttpLink } from "apollo-link-http";
+import { Route, Switch } from "react-router-dom";
 import Home from "./Home";
 import Login from "./Login";

+const uri = "http://localhost:5000/graphql";
+const context = setContext((_, { headers }) => {
+  return {
+    headers: { ...headers, token: localStorage.getItem("token") }
+  };
+});
+const link = context.concat(createHttpLink({ uri }));
+const cache = new InMemoryCache();
+const client = new ApolloClient({ cache, link });

 ReactDOM.render(
   <BrowserRouter>
+    <ApolloProvider client={client}>
       <Switch>
         <Route exact path="/" component={Home} />
         <Route path="/login" component={Login} />
       </Switch>
+    </ApolloProvider>
   </BrowserRouter>,
   document.getElementById("root")
 );

 // If you want your app to work offline and load faster, you can change
 // unregister() to register() below. Note this comes with some pitfalls.
 // Learn more about service workers: https://bit.ly/CRA-PWA
 serviceWorker.unregister();
```

`uri`にはバックエンドのエンドポイントを指定してください。
リクエストヘッダに`token`を`localStorage`から取得して、セットします。
今回は`Rails + API + GraphQL`のプロジェクトを`5000`番ポートで起動していました。

`react-apollo-hooks`を使う準備が整ったので早速ログイン機能から作って行きましょう。

# ログイン機能

以下のような`Mutation`をバックエンド側に用意しておきます。

```graphql
mutation {
  login(input: {loginid: "loginid", password: "password"}) {
    user {
      id
      accessToken {
        token
      }
    }
    result
  }
}
```

これにログインフォームに入力された値を埋め込みリクエストを送りたいと思います。

## 入力値をStateに格納

```diff:src/Login/index.tsx
-import React from "react";
+import React, { useState } from "react";

...
 const Login = () => {
+  const [loginid, setLoginid] = useState("");
+  const [password, setPassword] = useState("");

...

-<input type="text" />
+<input
+  type="text"
+  value={loginid}
+  onChange={e => setLoginid(e.target.value || "")}
+/>

...

-<input type="text" />
+<input
+  type="password"
+  value={password}
+  onChange={e => setPassword(e.target.value || "")}
+/>

...
```

## ログインボタンをログイン処理実行

```diff:src/Login/index.tsx
 import React, { useState } from "react";
+import { useMutation } from "react-apollo-hooks";
+import { withRouter } from "react-router";
+import { History } from "history";
+import gql from "graphql-tag";

+interface IProps {
+  history: History;
+}

+const Login = ({ history }: IProps) => {
...
+  const loginMutation = gql`
+    mutation login($loginid: String!, $password: String!) {
+      login(input: { loginid: $loginid, password: $password }) {
+        user {
+          accessToken {
+            token
+          }
+        }
+        result
+      }
+    }
+  `;
+
+  const [login] = useMutation(loginMutation, {
+    update: (_proxy, response) => {
+      if (response.data.login.result) {
+        localStorage.setItem(
+          "token",
+          response.data.login.user.accessToken.token
+        );
+        history.push("/");
+      } else {
+        alert("ログイン情報が不正です。");
+        setLoginid("");
+        setPassword("");
+      }
+    },
+    variables: { loginid, password }
+  });

   return (
...
-<input type="submit" value="ログイン" />
+<input type="submit" value="ログイン" onClick={() => login()} />
...

-export default Login;
+export default withRouter(Login);
```

ログインに成功（`response.data.login.result`が`true`）するとルートページに移動し、失敗すると`alert`を出す処理が出来ました。

ログインに成功した場合はレスポンスから取得したトークンを`localStorage`にセットしています。

`localStorage`にトークンをセットする処理が出来たので、既にログイン済みの場合、ルートページにリダイレクトする処理を作ります。

# ログイン済みユーザのルートページへ自動リダイレクト処理

以下のようなログイン済みユーザを取得する`Query`をバックエンド側に用意しておきます。

```gradle
{
  loggedUser {
    id
  }
}
```

レスポンスにユーザ情報が含まれている場合は、ログイン済み、`null`の場合は、未ログインと判断します。

```diff:src/Login/index.tsx
...
-import { withRouter } from "react-router";
+import { Redirect, withRouter } from "react-router";
...
-import { useMutation } from "react-apollo-hooks";
+import { useQuery, useMutation } from "react-apollo-hooks";
...

+  const loggedUserQuery = gql`
+    {
+      loggedUser {
+        id
+      }
+    }
+  `;

   const loginMutation = gql`
...
  const [login] = useMutation(loginMutation, {
...

+  const { loading, data } = useQuery(loggedUserQuery);

+  if (loading) {
+    return <div>Now Loading...</div>;
+  } else if (data.loggedUser) {
+    return <Redirect to="/" />;
+  }

   const login = () => {
...
```

ログインしているか問い合わせ中はローディングメッセージを表示し、ログインしていれば、ルートページにリダイレクト、そうでなければ、ログインページを表示することが出来るようになりました。

# 最後に

[`/graphiql`](http://localhost:5000/graphiql)でクエリを試す際、フロント側で定義しているクエリをコピペするだけで実行出来るので、嬉しいですね。
※引数がある場合は、`variables`を画面左下のフォームにて設定する必要があります。

# 参考文献

- [react-apollo-hooks - Github](https://github.com/trojanowski/react-apollo-hooks)
- [React Apolloのhooks対応のベータ版が公開！ - Qiita](https://qiita.com/daishi/items/b9cc8c2c18920a70a26f)