React ログイン機能作ってみた。(JSX)


# はじめに
### 結論：セキュリティ向上のため、CognitoやAuth0を使用することをお勧めします。

:::note alert
**【注意】**
今回は上記を使用しませんのでご了承ください。
**ご利用される際は、自己責任でご利用ください。**
:::

## 実行環境
:::note warn
reactの実行環境は各自でご用意ください。
`npx create-react-app`や`webpack`など
:::
## 今回使用する必須パッケージ
```json:package.json
"dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.9.0",
}
```

## ディレクトリ構造
:::note warn
構築した環境によって構造に違いがありますのでご了承ください。
:::
```
[root]/
    ┣ src/
    ┃   ┣ modules/
    ┃   ┃      ┗ fetch.js
    ┃   ┣ router/
    ┃   ┃      ┣ AuthContext.jsx
    ┃   ┃      ┗ CustomRouter.jsx
    ┃   ┣ views/
    ┃   ┃      ┣ pages/
    ┃   ┃      ┃     ┗ HomePage.jsx
    ┃   ┃      ┗ LoginPage.jsx
    ┃   ┣ App.jsx
    ┃   ┣ index.html
    ┃   ┗ index.js
    ┗ package.json
```

## index.jsでApp.jsxをレンダリング
```js:index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const target = document.getElementById('root')

createRoot(target).render(
    <App />
)
```

:::note 
ログイン認証結果などの格納場所を作るため、`AuthProvider`を一番外枠として記述します。
:::


```jsx:App.jsx
import React from "react";

import AuthProvider from "./router/AuthContext";
import CustomRouter from "./router/CustomRouter";

const App = () => {
    return(
        <AuthProvider>
            <CustomRouter />
        </AuthProvider>
    )
}
export default App
```

## 認証結果の格納場所を作成
① `createContext()`を使い値の格納場所を作成します。(値を下の階層に受け渡すための容器)
② ①を扱いやすいようにする関数
③ ブラウザの`localStrage`に保存する関数
④ ブラウザの`localStrage`から値を取得する関数
⑤ ログイン状態を管理するための認証基盤
⑥ ①の`AuthContext`に`value`として値を格納

```jsx:AuthContext.jsx
import React, { createContext, useState, useContext } from 'react'

// ①
const AuthContext = createContext();
// ②
export const useAuthContext = () => {
  return useContext(AuthContext)
}
// ③
export const setSession = (AuthInfo) => {
  localStorage.setItem('AuthInfo', JSON.stringify(AuthInfo))
}
// ④
export const getSession = () => {
  const userInfo = localStorage.getItem('AuthInfo')
  if (userInfo) {
    return JSON.parse(userInfo)
  } else {
    return {}
  }
}
// ⑤
const AuthProvider = ({ children }) => {
  const [LoggedIn, setLoggedIn] = useState(false)
  const [AuthInfo, setAuthInfo] = useState({})
  const [Loading, setLoading] = useState(false)

  return (
    // ⑥
    <AuthContext.Provider value={{
      LoggedIn,
      setLoggedIn,
      AuthInfo,
      setAuthInfo,
      Loading,
      setLoading
    }}>
      {children}
    </AuthContext.Provider>
  )
}
export default AuthProvider
```

## 認証結果を元に遷移させる機能の作成
① ログインしているかどうかのデータを受け取る
② `PrivateRoute` : ログインしていなければ、ログインページに遷移させる関数(URLを直接入力され際の対応)
③ `Login_check` : ログインしていれば、ログイン後のページに遷移させる関数(*今回の仕様ではログアウトするまで、ログイン画面は表示されません。)
④ ログインページに遷移した際に③の関数を実行する(自動遷移が不要の場合は省いてください)
⑤ ログインしている場合のみ、表示したいページに対して②の関数を使います。

```jsx:CustomRouter.jsx
import React from 'react'
import { BrowserRouter as Router, Navigate, NavLink, Route, Routes } from "react-router-dom";

import LoginPage from '../views/LoginPage'
import HomePage from '../views/pages/HomePage'
import { getSession, useAuthContext } from './AuthContext';

const CustomRouter = () => {
  // ①
  const {
    LoggedIn,
  } = useAuthContext()
  // ②
  const PrivateRoute = ({ children }) => {
    if (!LoggedIn && !getSession().id) {
      return <Navigate to={"/login"} />
    } else {
      return children
    }
  }
  // ③
  const Login_check = ({ children }) => {
    if (getSession().id) {
      return <Navigate to={"/"} />
    } else {
      return children
    }
  }

  return (
    <>
      <Router>
          <Routes>
            {/* ④ */}
            <Route path="/login" element={<Login_check><LoginPage /></Login_check>} />
            <Route path="*" element={<>PAGE NOT FOUND 404</>} />
            {/* ⑤ */}
            <Route path="/" element={<PrivateRoute><HomePage /></PrivateRoute>} />
          </Routes>
      </Router>
    </>
  )
}

export default CustomRouter
```
## 使用頻度が高そうなものはmoduleにする
受け取った値を使いやすいように整形してコールバック。
エラーの場合はエラー文をコールバック。
```js:fetch.js
export const fetch_Fnc = async (url, data) => {
  return await fetch(url, {
    method: 'POST',
    headers: {
      'Content-type': 'application/json',
    },
    body: JSON.stringify(data),
  })
    .then(Response => Response.json())
    .then(data => data)
    .catch(err => err)
}
```

## DB接続用のサーバー
ここでは、DBサーバーにアクセスして`email`と`password`が一致したUserデータを取得。
今回は一致したUserの`id`を使用していきます。

一致しなかった場合は、エラーメッセージをDBサーバー側から受け取る想定で記述しています。

:::note warn
サーバーに関しての記述は省かせていただきますので、各自でご用意してください。
:::

:::note alert
必要でない限り、全ての情報を取得する必要はありません。
今回はわかりやすいように`id`を使用していますが、JWT(Json Web Token)などの`token`を受け取ることをお勧めします。
:::
```js:例)
[{"id":"1","email":"example.com","password":"@^_^@",..."more"},]
```
## ログインページの作成
① `useAuthContext()`から値を受け取る
② `LoggedIn`の値が変わったら実行(ログインに成功していれば、`Loading`の値を`true`に変更)
③ `email`と`password`が正しければ、`result`に値が返ってくる
④ セッション管理として`localStrage`に値を格納
⑤ ログイン後に扱いやすいように`AuthInfo`に値を格納
⑥ ログインが成功したので`LogggedIn`を`true`に変更
⑦ 一致しなかった場合は、エラーメッセージをアラートする
⑧ `Loading`の値が`true`(ログインが成功している場合)はログイン後のページに遷移



```jsx:LoginPage.jsx
import React, { useEffect, useState } from 'react'
import { getSession, setSession, useAuthContext } from '../router/AuthContext'
import { fetch_Fnc } from '../modules/fetch'
import { Navigate } from 'react-router-dom'

const LoginPage = () => {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")

  // ①
  const {
    LoggedIn,
    setLoggedIn,
    AuthInfo,
    setAuthInfo,
    Loading,
    setLoading,
  } = useAuthContext()

  // ②
  useEffect(() => {
    if (LoggedIn||getSession().id) {
      setLoading(true)
    }
  }, [LoggedIn])


  const Login = async () => {
    //バリデーションチェック(超簡易版)
    if (!email) {
      return console.log('メールアドレスを入力してください')
    } else if (!password) {
      return console.log('パスワードを入力してください')
    }

    const data = { email: email, password: password }
    // URLは各自のサーバー環境に合わせてください
    const url = 'http://localhost:8080/login'
    const result = await fetch_Fnc(url, data)
    if (result[0].id) {
      // ④
      setSession(result[0])
      // ⑤
      setAuthInfo(result[0])
      // ⑥
      setLoggedIn(true)
    } else {
      // ⑦
      return alert(result)
    }
  }

  return (
    <>
      <div>LoginPage</div>
      <input type="email"
        value={email}
        placeholder='メールアドレス'
        onChange={(e) => setEmail(e.currentTarget.value)}
      />
      <input type="password"
        value={password}
        placeholder='パスワード'
        onChange={(e) => setPassword(e.currentTarget.value)}
      />
      <button onClick={Login}>ログイン</button>
      // ⑧
      {Loading ? <Navigate to={"/"} /> : ''}
    </>
  )
}

export default LoginPage
```

## ログイン後のページを用意

① ログインページに遷移
② `LoggedIn`を`false`に変更
③ `localStrage`から認証用データを削除

```jsx:HomePage.jsx
import React from "react";
import { NavLink } from "react-router-dom";
const HomePage = () => {
    return (
        <>
            <div>HomePage</div>

            <NavLink 
             // ①
             to={"/login"}
             onClick={() =>{
               // ②
               setLoggedIn(false)
               // ③
               localStorage.removeItem('AuthInfo')
            }}>
              ログアウト
            </NavLink>
        </>

    )
}
export default HomePage
```


# 完成


# 追記
TSX版も後日投稿予定です。