AmplifyとReactでCognito認証ページを作る(その4)

今回、4階部分(React/JSX)の後半である。前回はこちらから。今回、Authenticatorの開発を行う。

実現したいことのイメージ(6階建ての4階部分)

React(JSX)を使って、ログイン後のマイページトップ画面とユーザー情報変更画面を作っていく。

実現したい画面遷移

4階:Reactで画面を作る<Authenticator編>

Authenticatorで実現したいこと

逆引き的に考え、以下の3つを実現したい。Authenticatorのリファレンスを参考にしつつ進める。

  1. 全ページログイン必須とし、実ページと処理を分離する
  2. ニックネームなど他の属性を追加する
  3. 追加した属性を変更できるようにする

■Authenticator | Amplify UI for React
https://ui.docs.amplify.aws/react/connected-components/authenticator

具体的には以下のような画面遷移の構造を想定する。

画面遷移の構造

1.全ページログイン必須とし、実ページと処理を分離する

最初に悩んだ点が、別ページ(別コンポーネント)に分離した際にログイン情報をどのようにして引き渡すか、という点。「props」というパラメーターを用いて引き渡すことは可能だが、属性が増えれば増えるほど処理が煩雑になってしまう。

今回は、useAuthenticator という機能を使う。この機能により親のコンポーネントから子のコンポーネントに値を引き渡すことができる(後でわかるが、ReactのContextと同等の機能である)。

LoginPage → MyPage

最初に、LoginPage → MyPage の実装を行う。

以下のリファレンスを確認しながら進める。

■Headless | Amplify UI for React
https://ui.docs.amplify.aws/react/connected-components/authenticator/headless

変更前の親コンポーネントであるLoginPage.jsは以下の通り。前回作成したソースコードは以下の通り。変更したい箇所を削除する。

■/src/pages/LoginPage.js
import { Authenticator } from '@aws-amplify/ui-react';
import { Link } from "react-router-dom"; ←削除

const LoginPage = () => {
    return (
        <Authenticator>
        {({ signOut, user }) => (
          ↓削除
          <main>
            <h1>Hello {user.username}</h1>
            <button onClick={signOut}>Sign out</button>
            <p><Link to="/blank">ブランクページへ</Link></p>
          </main>
          ↑削除
        )}
        </Authenticator>
      );
}

export default LoginPage;

親コンポーネントにおいて、<Authenticator.Provider> 内で子コンポーネント(MyPage)を呼び出す。変更後のLoginPage.jsは以下の通り。

■/src/pages/LoginPage.js
import { Authenticator } from '@aws-amplify/ui-react';
import MyPage from "./MyPage"; ←追加

const LoginPage = () => {
    return (
        <Authenticator>
        {({ signOut, user }) => (
          ↓追加
          <Authenticator.Provider>
              <MyPage />
          </Authenticator.Provider>
          ↑追加
        )}
        </Authenticator>
      );
}

export default LoginPage;

新しく作る子コンポーネントのMyPage.jsは以下とする。

■/src/pages/MyPage.js
import { useAuthenticator } from '@aws-amplify/ui-react';
import { Link } from "react-router-dom";

const MyPage = () => {
    const { user, signOut } = useAuthenticator((context) => [context.user]);
    return (
        <main>
            <h1>Hello {user.username}</h1>
            <button onClick={signOut}>Sign out</button>
            <p><Link to="/user_edit">ユーザー情報変更</Link></p>
        </main>
    );
}

export default MyPage;

子コンポーネント内で、useAuthenticator フックでcontext.userを指定することで、ユーザー情報(user)を引き継ぐことができる。

2.ニックネームなど他の属性を追加する

追加できる全ての属性は以下に記載されている。

■Configuration | Amplify UI for React
https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration#sign-up-attributes

実際に組み込んで試してみる。

■/src/pages/LoginPage.js
import { Authenticator } from '@aws-amplify/ui-react';
import MyPage from "./MyPage";

const LoginPage = () => {
    return (
        <Authenticator signUpAttributes={[
            'address', // Not displayed
            'birthdate',
            'email',
            'family_name',
            'gender', // Not displayed
            'given_name',
            'locale', // Not displayed
            'middle_name', // Not used in Japan
            'name',
            'nickname',
            'phone_number',
            'picture', // Not displayed
            'preferred_username', // Not used in Japan
            'profile',
            'updated_at', // Not displayed
            'website',
            'zoneinfo', // Not displayed
            ]}>
        {({ signOut, user }) => (
          <Authenticator.Provider>
              <MyPage />
          </Authenticator.Provider>
        )}
        </Authenticator>
      );
}

export default LoginPage;
全ての属性を表示しようとした結果

先ほどのリファレンス「Sign Up Attributes」を確認すると、自動でレンダリングされない項目が存在するとある。

The Authenticator automatically renders most Cognito User Pools attributes, with the exception of addressgenderlocalepictureupdated_at, and zoneinfo. Because these are often app-specific, they can be customized via Sign Up fields.

Configuration | Amplify UI for React

以下に詳しい解説があるが、今回はスキップする。

■User pool attributes – Amazon Cognito
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html

■Final: OpenID Connect Core 1.0 incorporating errata set 1
https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

実際にアカウント作成をしようとすると、なぜかProfile欄で「URLを入力してください。」と怒られる。

ProfileとWebsiteは怪しい

Cognitoを確認すると、Websiteは登録されていない。

Cognitoに表示されたユーザー属性

原因はよくわからないがProfileとWebsiteは使わない方が無難かもしれない。

また、日本では馴染みのない「ミドルネーム」などを除くと、デフォルトで使える属性は以下になる。

  • birthday
  • email
  • family_name
  • given_name
  • name
  • nickname
  • phone_number

修正後のソースは以下の通り。

■/src/pages/LoginPage.js
import { Authenticator } from '@aws-amplify/ui-react';
import MyPage from "./MyPage";

const LoginPage = () => {
    return (
        <Authenticator signUpAttributes={[
            'birthdate',
            'email',
            'family_name',
            'given_name',
            'name',
            'nickname',
            'phone_number',
            ]}>
        {({ signOut, user }) => (
          <Authenticator.Provider>
              <MyPage />
          </Authenticator.Provider>
        )}
        </Authenticator>
      );
}

export default LoginPage;

MyPage.jsを修正して属性の値を確かめる。

user.attributes.birthdate のようにattributesで修飾するのがポイントである。ちなみに、usernameは自動でユニークに振られるIDのため変更はできない。

■/src/pages/MyPage.js
import { useAuthenticator } from '@aws-amplify/ui-react';
import { Link } from "react-router-dom";

const MyPage = () => {
    const { user, signOut } = useAuthenticator((context) => [context.user]);
    return (
        <main>
            <h1>Hello {user.username}</h1>

            <li>birthdate:{user.attributes.birthdate}</li>
            <li>email:{user.attributes.email}</li>
            <li>family_name:{user.attributes.family_name}</li>
            <li>given_name:{user.attributes.given_name}</li>
            <li>name:{user.attributes.name}</li>
            <li>nickname:{user.attributes.nickname}</li>
            <li>phone_number:{user.phone_number}</li>

            <button onClick={signOut}>Sign out</button>
            <p><Link to="/user_edit">ユーザー情報変更</Link></p>
        </main>
    );
}

export default MyPage;

以下のように表示される。

ユーザー属性を表示

phone_numberは、「未認証」なので表示されていない。認証する手順もあるとは思うが脇道に逸れるため、今回はphone_numberを属性変更の対象外とする。

3.追加した属性を変更できるようにする

UserEdit → UserEditPage

次に、UserEdit → UserEditPage の実装を行う。

■/src/pages/UserEdit.js
import { Authenticator } from '@aws-amplify/ui-react';
import UserEditPage from "./UserEditPage";

const UserEdit = () => {
    return (
        <Authenticator signUpAttributes={[
            'birthdate',
            'email',
            'family_name',
            'given_name',
            'name',
            'nickname',
            'phone_number',
            ]}>
        {({ signOut, user }) => (
            <Authenticator.Provider>
                <UserEditPage />
            </Authenticator.Provider>
        )}
        </Authenticator>
    );
}

export default UserEdit;

子コンポーネントのUserEditPage.js、まずは初期値を表示する実装を行う。

■/src/pages/UserEditPage.js
import { useAuthenticator } from '@aws-amplify/ui-react';

const UserEditPage = () => {
    const { user, signOut } = useAuthenticator((context) => [context.user]);
    return (
        <main>
            <li>birthdate: <input defaultValue={user.attributes.birthdate} /></li>
            <li>email: <input defaultValue={user.attributes.email} /></li>
            <li>family_name: <input defaultValue={user.attributes.family_name} /></li>
            <li>given_name: <input defaultValue={user.attributes.given_name} /></li>
            <li>name: <input defaultValue={user.attributes.name} /></li>
            <li>nickname: <input defaultValue={user.attributes.nickname} /></li>
            
            <p><button>ユーザー情報を変更する</button></p>
            <p><button onClick={signOut}>ログアウトする</button></p>
        </main>
    );
}

export default UserEditPage;
属性(ニックネーム)のステートを保持

例としてニックネームを変更する。ReactのフックであるuseState() を使い、ユーザーが入力した値を保持する。また、onChangeイベント発生時にニックネームを最新化する。

このあたりの話は調べれば出てくるが、React話のうんちくから入ると非常に入りにくいと(個人的には)思う。まずは動かしてみた後に確認すると理解が早い。

ちなみに、動作確認用に変更したニックネームをコンソールに表示するようにしている。

■/src/pages/UserEditPage.js
import { useAuthenticator } from '@aws-amplify/ui-react';
import { useState } from "react"; ←追加

const UserEditPage = () => {
    const { user, signOut } = useAuthenticator((context) => [context.user]);
    const [nickname, setNickname] = useState(user.attributes.nickname); ←追加

    console.log(nickname); ←追加

    return (
        <main>
            <li>birthdate: <input defaultValue={user.attributes.birthdate} /></li>
            <li>email: <input defaultValue={user.attributes.email} /></li>
            <li>family_name: <input defaultValue={user.attributes.family_name} /></li>
            <li>given_name: <input defaultValue={user.attributes.given_name} /></li>
            <li>name: <input defaultValue={user.attributes.name} /></li>
            <li>nickname: <input defaultValue={nickname} onChange={e => setNickname(e.target.value)} /></li> ←変更
            
            <p><button>ユーザー情報を変更する</button></p>
            <p><button onClick={signOut}>ログアウトする</button></p>
        </main>
    );
}

export default UserEditPage;

App.js にRouteを追加する。

■/src/App.js
import { Amplify } from 'aws-amplify';
import '@aws-amplify/ui-react/styles.css';

import { Route, Routes, BrowserRouter } from "react-router-dom";
import BlankPage from "./pages/BlankPage"; ←削除
import LoginPage from "./pages/LoginPage";
import UserEdit from "./pages/UserEdit"; ←追加

import awsExports from './aws-exports';
Amplify.configure(awsExports);

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<LoginPage />} />
        <Route path="/blank" element={<BlankPage />} /> ←削除
        <Route path="/user_edit" element={<UserEdit />} /> ←追加
      </Routes>
    </BrowserRouter>
  );
}

export default App;

以下のようにコンソールに表示される。

ユーザー情報の入力を保持する
ユーザー属性(ニックネーム)の更新

ユーザー情報を更新するメソッドを追加し、ボタンクリックでそのメソッドを呼び出す。以下を参考に実装する。

■Authentication – Managing user attributes – JavaScript – AWS Amplify Docs
https://docs.amplify.aws/guides/authentication/managing-user-attributes/q/platform/js/#writing-and-updating-standard-attributes

■/src/pages/UserEditPage.js
import { useAuthenticator } from '@aws-amplify/ui-react';
import { useState } from "react";
import { Auth } from 'aws-amplify'; ←追加

const UserEditPage = () => {
    const { user, signOut } = useAuthenticator((context) => [context.user]);
    const [nickname, setNickname] = useState(user.attributes.nickname);

    console.log(nickname);
    ↓追加
    async function updateUserInfo() {
        let result = null;
        try {
            const user = await Auth.currentAuthenticatedUser();
            result = await Auth.updateUserAttributes(user, {
                nickname: nickname
            });
        } catch(error) {
            console.log('error update: ', error);
        }
        if (result === 'SUCCESS') {
            alert("ユーザー情報を更新しました!");
        }
        else {
            alert("ユーザー情報の更新に失敗しました・・・");
        }
        return result;
    }
    ↑追加
    return (
        <main>
            <li>birthdate: <input defaultValue={user.attributes.birthdate} /></li>
            <li>email: <input defaultValue={user.attributes.email} /></li>
            <li>family_name: <input defaultValue={user.attributes.family_name} /></li>
            <li>given_name: <input defaultValue={user.attributes.given_name} /></li>
            <li>name: <input defaultValue={user.attributes.name} /></li>
            <li>nickname: <input defaultValue={nickname} onChange={e => setNickname(e.target.value)} /></li>
            
            <p><button onClick={() => updateUserInfo()}>ユーザー情報を変更する</button></p> ←変更
            <p><button onClick={signOut}>ログアウトする</button></p>
        </main>
    );
}

export default UserEditPage;

ニックネームを変更して更新することができた。

ユーザー情報の更新に成功

ニックネーム以外も同様に実装することで、ユーザー情報の更新が可能になる。

次回は、見た目を整えるべくCSSを追記する予定である。