Next.jsでログインフォームを作成してみる

# はじめに
Next.jsにて、ログインフォームを作成してみたので備忘録です。

# 環境について
Next.js 14.0.4
App Router を用いています
Tailwind css を用いています

# ログインフォームの内容
今回作成したログインフォームには以下のようなフィールドが含まれます。

- メールアドレス: ユーザーのメールアドレスを入力するフィールド
- パスワード: ユーザーのパスワードを入力するフィールド
- ログインボタン: フォームを送信するためのボタン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3760062/4f5c2124-29d4-0880-286d-2330f27290df.png)

ログインボタンが押されたら、入力したメールアドレスとパスワードの内容でAPIを叩くようにしています。


#### プロジェクトのディレクトリ構成
今回のプロジェクトでは、以下のようなディレクトリ構成を採用しています。
(※ログイン機能の部分のみ書いてます)

``` 
src/
├── app/
│   ├── (authenticated)/
│   │   ├── login/
│   │   │   ├── _api/
│   │   │   │   └── login.ts
│   │   │   ├── _components/
│   │   │   │   └── LoginForm/
│   │   │   │       └── LoginForm.tsx
│   │   │   └── page.tsx

```

App Routerの説明は省きます。（※下記記事などを参照）

https://zenn.dev/yamadadayo123/articles/6cb4f586de0183

# 実装手順
### 1. ログインフォームの作成
まず、_components/LoginForm ディレクトリに LoginForm.tsx という新しいコンポーネントを作成します。
以下のコードは、今回作成したログインフォームの構造です。

``` React:src/app/login/_components/LoginForm/LoginForm.tsx
'use client'

import { useFormState } from 'react-dom'
import Button from '../../../../components/Button/LoginButton'
import TextField from '../../../../components/TextField/TextField'
import { login } from '../../_api/login'

export default function LoginForm() {
  const [state, formAction] = useFormState(login, { errorMessages: '' })
  return (
    <form
      action={formAction}
      className='flex w-96 flex-col gap-5 rounded bg-gray-3C444D px-4 py-10'
    >
      {state.errorMessages && (
        <div className='rounded px-4 py-2 ring-1 ring-red-FF6B6B'>
          <p className='text-sm font-normal text-red-FF6B6B'>
            {state.errorMessages}
          </p>
        </div>
      )}
      <TextField
        label='メールアドレス'
        description='50文字以内で入力してください'
        maxLength={50}
        name='email'
        type='email'
        required
        placeholder='sample@example.com'
      />
      <TextField
        label='パスワード'
        description='50文字以内で入力してください'
        maxLength={50}
        name='password'
        type='password'
        required
      />
      <LoginButton />
    </form>
  )
}

```

useFormStateフックを使用してフォームの状態を管理しています。
（※useFormStateは、フォームの状態や送信アクションを簡単に管理するためのものです。）

フォーム送信時には、ユーザーが入力したメールアドレスとパスワードをlogin関数に渡し、サーバーにリクエストを送信するようにします。
サーバーで何かエラーが起きた際には、下記の場所でエラーメッセージが表示されるようになっています。
```
{state.errorMessages && (
        <div className='rounded px-4 py-2 ring-1 ring-red-FF6B6B'>
          <p className='text-sm font-normal text-red-FF6B6B'>
            {state.errorMessages}
          </p>
        </div>
      )}
```

LoginButtonやTextFieldは、componentsディレクトリでそれぞれ定義していますが、今回は内容についての説明は割愛します。


### 2. ログインAPIの設定
ログイン処理を行うlogin関数を定義します。ここでは、ユーザーの入力したメールアドレスとパスワードをサーバーに送信し、認証が成功すればトークンをクッキーに保存し、特定のページにリダイレクトする流れを実装しています。

※今回作成したログインAPIは、データベースの管理者ユーザーテーブルにレコードが存在するユーザーの情報をリクエストすると、レスポンスで、トークンとユーザーの情報が返ってくるようになっています。

``` React:src/app/login/_api/login.ts
'use server'

import { redirect } from 'next/navigation'
import { setCookie } from '../../../utils/cookies/setCookie'
import { CookieName } from '../../../utils/cookies/cookie'

export interface LoginResponse {
  errorMessages: string
}

export async function login(_: LoginResponse, formData: FormData) {
  try {
    const res = await fetch('http://loginTest:8080/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        email: formData.get('email'),
        password: formData.get('password')
      }),
      cache: 'no-store'
    })

    const data = await res.json()

    if (!res.ok) {
      console.error(data)
      throw new Error('server returned an error')
    }

    setCookie(CookieName.TOKEN, data.token)
  } catch (error) {
    let errorMessages = '予期せぬエラーが発生しました'
    if (error instanceof Error && error.message === 'server returned an error') {
      errorMessages = 'メールアドレスもしくはパスワードが違います'
    }

    return {
      errorMessages: errorMessages
    } as LoginResponse
  }

  // ログインが成功したらトップページへ
  redirect('/top')
}

```

setCookie関数などで、Next.jsのcookies() APIを使用して、クッキーを設定したり取得したりしていますが、ここの紹介も割愛します。

### 3. ログインページの作成

``` React:src/app/login/page.tsx
import LoginForm from './_components/LoginForm/LoginForm'

export default function Login() {
  return (
    <main className='flex h-screen items-center justify-center'>
      <LoginForm />
    </main>
  )
}

```

さっき作成したLoginFormコンポーネントを記載して終了です。

※諸々の細かい説明などは、記事を更新するタイミングなどでまた書いていこうかなと思っています。

# まとめ
今回は、ざっくりですがNext.jsを使ったログインフォームを作成しました。
APIを用いたユーザー認証機能などは基本的に決まった形で実装できるはずです。
今後も何か新しい機能など作成した際に、フォーマットとしてここに記録していこうと思います。