Next.js + Capacitorによるクロスプラットフォームアプリの紹介 – ③ログイン

React

login.tsx

以下のコードは、Next.js アプリケーションの「ログイン」ページの実装の一部であり、いくつかの重要なポイント(クライアントサイドレンダリング、動的インポート、Head コンポーネントなど)があります。それぞれを詳しく解説します。

'use client'

import dynamic from 'next/dynamic'
import Head from 'next/head'

const Content = dynamic(
  () => import('./components/Content').then((module) => module.Content),
  {
    ssr: false,
  }
)

export default function Login() {
  return (
    <>
      <Head>
        <title>Top</title>
      </Head>
      <Content />
    </>
  )
}

1. 'use client' の意味

'use client'

このディレクティブは、Next.js 13 以降で導入された App Router 構造におけるものです。この宣言は、このファイルがクライアントサイドで実行されることを示します。通常、Next.js はサーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)を自動的に分けて処理しますが、'use client' を使うことで、明示的にクライアント側でのみ実行するコンポーネントや処理を指定することができます。

このファイルでは、クライアントサイドでのインタラクティブな処理が必要であることを示しており、クライアントサイドで動的にコンポーネントをレンダリングする設定がされています。

2. dynamic の使用と ssr: false の指定

import dynamic from 'next/dynamic'

const Content = dynamic(
  () => import('./components/Content').then((module) => module.Content),
  {
    ssr: false,
  }
)
  • dynamic: Next.js の dynamic 関数は、コンポーネントを動的にインポート(遅延読み込み)するために使います。これにより、アプリケーションの初期読み込み時間を短縮し、パフォーマンスを最適化します。
  • ssr: false: ssr(Server-Side Rendering)の設定を false にすることで、このコンポーネントがサーバーサイドではレンダリングされず、クライアントサイドでのみ動的に読み込まれることを指定しています。Content コンポーネントは、クライアントサイドでのみ必要な処理を行うため、サーバーではレンダリングされません。
  • 動的インポートの利点: このアプローチは、クライアントサイドでのみ実行される機能(例: ブラウザ API を利用するもの)を持つコンポーネントに適しています。サーバーサイドでは不要なクライアント依存のコードを回避し、必要になったときにのみクライアントサイドでレンダリングすることで、パフォーマンス向上を図ります。

3. Head コンポーネント

import Head from 'next/head'

<Head>
  <title>Top</title>
</Head>
  • Head コンポーネント: このコンポーネントは、Next.js で HTML の <head> 要素に対するカスタマイズを行うために使用されます。ここでは、ページの <title> 要素を設定しています。これにより、ブラウザのタブに表示されるページのタイトルが「Top」に設定されます。

4. Login コンポーネント

export default function Login() {
  return (
    <>
      <Head>
        <title>Top</title>
      </Head>
      <Content />
    </>
  )
}
  • Login 関数コンポーネント: このコンポーネントは、ログインページのエクスポートデフォルトコンポーネントであり、以下の要素を含みます:
  • <Head>: 上記の通り、<title> を設定します。
  • <Content />: 動的に読み込まれる Content コンポーネントをレンダリングします。このコンポーネントは ssr: false で指定されているため、クライアントサイドでのみ表示されます。

Content.tsx

以下のコードのログイン機能に関する部分は、Auth0 を使用した認証の実装がメインです。具体的には、Auth0 の loginWithRedirect メソッドを使い、ユーザーを Auth0 のログインページにリダイレクトし、その後にログインのコールバックを処理する仕組みが含まれています。

'use client'

import { useAuth0 } from '@auth0/auth0-react'
import { App as CapApp } from '@capacitor/app'
import { Browser } from '@capacitor/browser'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'

import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { getDeviceInfo } from '@/lib/utils'

import AppleIcon from '@/assets/icons/apple.svg'
import GoogleIcon from '@/assets/icons/google.svg'
import LineIcon from '@/assets/icons/line.svg'
import LockIcon from '@/assets/icons/lock.svg'
import ProfileIcon from '@/assets/icons/profile.svg'

const Content = (): JSX.Element => {
  const intl = useIntl()
  const { loginWithRedirect, handleRedirectCallback } = useAuth0()
  const router = useRouter()

  const handleLogin = async () => {
    try {
      const deviceInfo = await getDeviceInfo()
      console.log("here12", deviceInfo)

      if (deviceInfo.platform === 'web') {
        await loginWithRedirect()
        return
      }

      await loginWithRedirect({
        async openUrl(url) {
          await Browser.open({
            url,
            windowName: '_self',
          })
        },
      })
    } catch (error) {
      console.error(error)
    }
  }

  useEffect(() => {
    const handleMobileRedirectCallback = async () => {
      const deviceInfo = await getDeviceInfo()
      console.log("here1:", deviceInfo)

      if (deviceInfo.platform === 'web') return

      // Handle the 'appUrlOpen' event and call `handleRedirectCallback`
      CapApp.addListener('appUrlOpen', async ({ url }) => {
        if (
          url.includes('state') &&
          (url.includes('code') || url.includes('error'))
        ) {
          await handleRedirectCallback(url).then(() => {
            router.replace('/validate-login')
          })
        }
        // No-op on Android
        await Browser.close()
      })
    }

    handleMobileRedirectCallback()
  }, [handleRedirectCallback, router])

  return (
    <>
      <div className="flex items-center gap-4">
        <Image src="/images/logo-dark.svg" width={80} height={80} alt="Logo" />

        <h1 className="text-4xl font-bold text-neutral-10">
          <FormattedMessage id="general.company_name" />
        </h1>
      </div>
      <article className="mt-8">
        <h2 className="text-neutral-40">
          <FormattedMessage id="page.login.form_title" />
        </h2>

        <form className="mt-4">
          <div className="flex flex-col gap-6">
            <Input
              type="email"
              placeholder="Your email"
              icon={<ProfileIcon width={14} />}
            />

            <Input
              type="password"
              placeholder={intl.formatMessage({
                id: 'page.login.password_placeholder',
              })}
              icon={<LockIcon width={14} />}
            />
          </div>

          <div className="mt-6 flex flex-col items-end gap-3">
            <Link href="/forgot-email" className="text-sm">
              <FormattedMessage id="page.login.forgot_email" />
            </Link>
            <Link href="/forgot-password" className="text-sm">
              <FormattedMessage id="page.login.forgot_password" />
            </Link>
          </div>

          <div className="mt-10 flex justify-center">
            <Button
              size="lg"
              type="button"
              className="w-48"
              data-testid="login-button"
              onClick={handleLogin}
            >
              <FormattedMessage id="general.button_login" />
            </Button>
          </div>

          <p className="mt-8 text-center text-sm text-neutral-30">
            <FormattedMessage id="page.login.login_with" />
          </p>
        </form>
        <div
          role="group"
          aria-label="other login options"
          className="mt-2 flex justify-center gap-4"
        >
          <Button variant="outline" className="w-28">
            <GoogleIcon width={20} />
            <span className="sr-only">Google</span>
          </Button>
          <Button variant="outline" className="w-28">
            <LineIcon width={20} />
            <span className="sr-only">Line</span>
          </Button>
          <Button variant="outline" className="w-28">
            <AppleIcon width={20} />
            <span className="sr-only">Apple</span>
          </Button>
        </div>

        <p className="mt-8 text-center">
          <FormattedMessage
            id="page.login.dont_have_account"
            values={{
              a: (...chunks) => <Link href="/create-account">{chunks}</Link>,
            }}
          />
        </p>

        <div className="mt-20 text-center">
          <Link href="/top" className="italic">
            <FormattedMessage id="general.top" />
          </Link>
        </div>
      </article>
    </>
  )
}

export { Content }

1. useAuth0 フックの使用

const { loginWithRedirect, handleRedirectCallback } = useAuth0()
  • useAuth0: これは Auth0 の React SDK が提供するフックで、ログインやログアウト、認証状態の管理などを行う機能を提供します。
  • loginWithRedirect: このメソッドは、ユーザーを Auth0 のホスティングするログインページにリダイレクトし、ログイン処理を行います。ログインが成功した後、Auth0 が指定されたリダイレクト URL に戻ります。
  • handleRedirectCallback: ログイン後に、Auth0 のコールバック URL にリダイレクトされた際に、このメソッドを使用して認証トークンを取得し、ログインプロセスを完了します。

2. ログイン処理 (handleLogin)

const handleLogin = async () => {
  try {
    const deviceInfo = await getDeviceInfo()

    if (deviceInfo.platform === 'web') {
      await loginWithRedirect()
      return
    }

    await loginWithRedirect({
      async openUrl(url) {
        await Browser.open({
          url,
          windowName: '_self',
        })
      },
    })
  } catch (error) {
    console.error(error)
  }
}
  • handleLogin 関数: この関数は、ユーザーがログインボタンをクリックしたときに呼び出され、Auth0 の認証ページへのリダイレクトを処理します。
  1. getDeviceInfo の呼び出し:
    • この関数は、デバイス情報(platform)を取得します。これは、現在の環境がウェブブラウザかモバイルデバイスかを判別するために使用されます。
  2. プラットフォームの確認:
    • deviceInfo.platform === 'web' の場合、loginWithRedirect をそのまま呼び出します。この場合、通常のウェブブラウザ上でのリダイレクト処理が行われます。
  3. モバイルデバイスの場合:
    • モバイルデバイスの場合は、loginWithRedirect にカスタムオプションを渡します。openUrl というカスタム関数を定義しており、Browser.open を使って、Capacitor のブラウザプラグインを利用し、Auth0 のログインページをモバイルブラウザで開きます。

3. ログイン後のリダイレクト処理 (useEffect)

useEffect(() => {
  const handleMobileRedirectCallback = async () => {
    const deviceInfo = await getDeviceInfo()

    if (deviceInfo.platform === 'web') return

    // Handle the 'appUrlOpen' event and call `handleRedirectCallback`
    CapApp.addListener('appUrlOpen', async ({ url }) => {
      if (
        url.includes('state') &&
        (url.includes('code') || url.includes('error'))
      ) {
        await handleRedirectCallback(url).then(() => {
          router.replace('/validate-login')
        })
      }
      await Browser.close()
    })
  }

  handleMobileRedirectCallback()
}, [handleRedirectCallback, router])
  • useEffect フック: このフックは、コンポーネントの初回レンダリング時に実行され、モバイルデバイスでのリダイレクト処理を監視します。
    • [handleRedirectCallback, router] は、useEffect フックの依存配列です。この依存配列は、useEffect の中で使用している変数や関数に基づいて、どのタイミングで useEffect の処理が再実行されるかを決定します。
    • handleRedirectCallback は、Auth0 の useAuth0 フックから提供される関数で、Auth0 による認証のリダイレクト処理が終わった後に、認証トークンの検証やユーザーの認証状態を更新するために使われます。この関数が依存配列に含まれていることで、handleRedirectCallback の参照が変更された場合に useEffect が再度実行されます。handleRedirectCallback の実装や参照が変わったときに、それを反映するために useEffect が再度実行され、最新の handleRedirectCallback を使ってモバイルデバイス上でのリダイレクト処理が正しく行われるようにしています。
    • router は、Next.js の useRouter フックから提供されるルーティング関連のオブジェクトで、ユーザーを特定のページにリダイレクトしたり、ページ遷移を制御するために使います。このコードでは、ログイン処理が完了した後に、router.replace('/validate-login') を呼び出して、/validate-login ページに遷移させています。router の参照が変わったとき、例えばユーザーが別のページに移動した場合や、router に新しい挙動が追加された場合に、useEffect が再度実行されます。これにより、最新の router オブジェクトを使って、リダイレクト処理が適切に機能するようにしています。
    • 再レンダリング時の再実行の防止: useEffect は、依存配列に含まれる値が変更された場合にのみ再実行されます。つまり、handleRedirectCallbackrouter が変更されない限り、useEffect 内の処理はコンポーネントが再レンダリングされても再実行されません。
    • 正しい値の参照: 依存配列に含めることで、useEffect 内で常に最新の handleRedirectCallbackrouter を使えるようにし、意図しない古い参照を使用することを防ぎます。
  • CapApp.addListener('appUrlOpen'): このリスナーは、モバイルアプリが URL を開いたときに発火し、リダイレクト先の URL に含まれる認証情報(statecode パラメータ)をチェックします。
    • handleRedirectCallback(url): このメソッドが呼び出され、Auth0 からのリダイレクト URL を処理し、認証情報をもとにユーザーのログイン状態を確立します。
    • router.replace('/validate-login'): 認証後、/validate-login ページにリダイレクトして、ログインが成功したことを通知します。

4. ログインボタンのクリックイベント

<Button
  size="lg"
  type="button"
  className="w-48"
  data-testid="login-button"
  onClick={handleLogin}
>
  <FormattedMessage id="general.button_login" />
</Button>
  • onClick={handleLogin}: このボタンがクリックされると、上記の handleLogin 関数が呼び出され、ログイン処理が開始されます。

関連記事

カテゴリー

アーカイブ

Lang »