20200929のReactに関する記事は6件です。

formikとreact-hook-formの比較-フォームを動的に増やす実装

目的

formikとreact-hook-formで動的に項目を増減させる書き方を調べたのでメモ。
主に以下の機能についての検証。

  • formikのFieldArray
  • react-hook-formのuseFieldArray

前提

  • Visual Studio Code 1.49.2
  • react 16.13.1
  • typescript 3.7.5
  • formik 2.1.5
  • react-hook-form 6.8.6
  • 姓、名の2つのデータを複数入力できるようにする。
  • 追加ボタンで姓、名の入力欄を増やす事ができるようにする。

サンプルデータ

formのinputイメージ

const initialValues = {
  listName: "",
  table: {
    rows: [
      { firstName: "sato", lastName: "taro" },
      { firstName: "suzuki", lastName: "jiro" }
    ]
  }
};

最終的に作ったもの

スクリーンショット2020-09-29 19.51.17.png

https://codesandbox.io/s/exciting-chatterjee-n68j3
上半分がformikの実装サンプルで、
下半分がreact-hook-formのサンプル。

ざっくり結論

formikはtypescriptの型がanyになってしまう。componentの定義もツラかった。
react-hook-formなら型定義できるが、hookの使い方が複雑。
typescriptの利用を前提にした場合、個人的な好みはreact-hook-form。

内容

1. formikのFieldArrayについて

https://formik.org/

form周りはどうしても複雑になりがちですが、
tutorialでとても分かりやすく使い方を定義してくれています。
https://formik.org/docs/tutorial

そんなformik公式ページですが、FieldArrayの解説になると分かりにくい点が多いです。
https://formik.org/docs/api/fieldarray

 import React from 'react';
 import { Formik, Form, Field, FieldArray } from 'formik';

 export const FriendList = () => (
   <div>
     <h1>Friend List</h1>
     <Formik
       initialValues={{ friends: ['jared', 'ian', 'brent'] }}
       // renderはDeprecatedでは?
       render={({ values }) => (
         <Form>
           <FieldArray
             name="friends"
             // ここでもrender
             render={arrayHelpers => (
               <div>
                 {values.friends && values.friends.length > 0 ? (
                   values.friends.map((friend, index) => (
                   // 省略
                   ))
                 )}
                 <div>
                   <button type="submit">Submit</button>
                 </div>
               </div>
             )}
           />
         </Form>
       )}
     />
   </div>
 );

※公式サンプルを一部省略し、コメントを追加

2系ではDeprecatedとなっているはずのrender関数が使われています。
しかも2回も。
そのため、componentが分離できず階層が深くて見通しが悪い実装になっています。

しかし、処理を追っていくとrenderの必要性が薄れてきます。
Formikタグのrenderはvaluesを、FieldArrayタグのrenderはarrayHelpersを取得するためのようですが、公式ページを読み進めると次のような書き方も提示されています。

 import React from 'react';
 import { Formik, Form, Field, FieldArray } from 'formik'


 export const FriendList = () => (
   <div>
     <h1>Friend List</h1>
     <Formik
       initialValues={{ friends: ['jared', 'ian', 'brent'] }}
       onSubmit={...}
       render={formikProps => (
         <FieldArray
           name="friends"
           component={MyDynamicForm}
         />
       )}
     />
   </div>
 );


 // In addition to the array helpers, Formik state and helpers
 // (values, touched, setXXX, etc) are provided through a `form`
 // prop
 export const MyDynamicForm = ({
   move, swap, push, insert, unshift, pop, form
 }) => (
  <Form>
   {/**  whatever you need to do */}
  </Form>
 );

※公式サンプルをそのまま貼り付け

ポイントは以下の3点です。

  • FieldArrayのpropsにcomponentを設定できる。
  • 上記のcomponentには引数でhelpersを受け取る。
  • helpers.form.valuesとたどればvaluesが取得できる

どうやらrenderで取得せずともvaluesやhelperを取得できるらしい。
ということで、FieldArrayでcomponentを使って実装してみました。

2. formikのFieldArrayとcomponentを使った実装

ただ、実装してみると、思ったほどうまくいきません。
typescript周りで面倒な事が起こります。

実装のコメントにも書いていますが、componentに渡すFCの定義がエラーになってしまいます。

// 親コンポーネント
const FormikPart: React.FC = () => {
  return (
    <>
      <h2>Formik array</h2>
      <Formik
        initialValues={initialValues}
        onSubmit={(values, helper) => {
          console.log(values);
        }}
      >
        <Form>
          <FieldArray name="table.rows" component={ArrayInput} />
          <button type="submit">submit</button>
        </Form>
      </Formik>
    </>
  );
};

// 子コンポーネント
// <void | FieldArrayRenderProps>を受け取れないとerrorになる
const ArrayInput: React.FC<void | FieldArrayRenderProps> = (props) => {
  // propsの型がReact.PropsWithChildren<void | FieldArrayRenderProps>になってしまうので
  // as で型を定義する
  const { form, name, push } = props as React.PropsWithChildren<
    FieldArrayRenderProps
  >;

  // name = table.rows
  // getInを使うと"table.rows"をハードコードする場所が減らせる
  // table.rowsの配列の値は取得できるが、型はany
  const array = getIn(form.values, name);

  // 型がanyなのでチェック等を書く必要がある
  if (!(Array.isArray(array) && array.length > 0)) return null;

  const list = array.map((val, index) => (
    <div key={index}>
      <Field name={`${name}[${index}].firstName`} type="text" />
      <Field name={`${name}[${index}].lastName`} type="text" />
    </div>
  ));
  return (
    <>
      {list}
      <button
        type="button"
        // pushの引数も型もanyなので型チェックはされない
        onClick={() => push({ firstName: "tanaka", lastName: "saburo" })}
      >
        add at last
      </button>
    </>
  );
};

コード全体が見たい方はこちら
https://codesandbox.io/s/exciting-chatterjee-n68j3?file=/src/components/FormikPart.tsx

FieldArrayRenderPropsはformikから提供されているFieldArrayの引数の型です。
本来ならReact.FC<FieldArrayRenderProps>のようにFCを定義したいのですが、
propsのcomponentの型定義がReact.ComponentType<T | void>となっておりvoidの指定をしないとtypescriptから怒られてしまいます。
React.ComponentType<void>という定義にどんな意味があるのかはよくわかりませんが...仕方なく上記のように型を定義し、引数のpropsはasで型を再度定義します。

また、formから取得した入力値も型がanyになってしまい、firstNameやlastNameといったオブジェクト構造のインテリセンスも効かないため、気をつけて実装する必要があります。

こうしたtypescriptの型定義の難しさを避けるために、別のライブラリも試してみることにしました。

3. react-hook-formのuseFieldArrayを使った実装

typescriptでよりシンプルに書くために、新しいライブラリであるreact-hook-formにトライしました。

https://react-hook-form.com/jp/

react-hook-formのドキュメントはとてもオシャレです。
formikのFieldArrayに相当するのはuseFieldArrayというhookです。

https://react-hook-form.com/jp/api#useFieldArray

必要な関数は最初からhookで提供されているので、renderの引数やcomponentの分離といったことは考えなくて大丈夫でした。

// 親コンポーネント
const ReactHookFormPart: React.FC = () => {
  const methods = useForm<FormInputs>({ defaultValues });
  const onSubmit = (data: FormInputs) => console.log(data);

  return (
    <>
      <h2>React Hook Form array</h2>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <ArrayInput />
          <button type="submit">submit</button>
        </form>
      </FormProvider>
    </>
  );
};

// 子コンポーネント
const name = "table.rows";
const ArrayInput: React.FC = () => {
  // hook利用時に型を指定する必要はあるが、型定義ができる
  const { control, register } = useFormContext<FormInputs>();
  // useFieldArrayでは配列の要素の型を指定する
  const { fields, append } = useFieldArray<TableRows>({
    control,
    // name(table.rows)を指定することで,fieldsにtable.rowsの値が入る
    name
  });

  const list = fields.map((item, index) => (
    <div key={item.id}>
      <input
        name={`${name}[${index}].firstName`}
        // registerはregister()としてmap内で実行する必要がある
        ref={register()}
        // itemも型定義されている
        defaultValue={item.firstName}
      />
      <input
        name={`${name}[${index}].lastName`}
        ref={register()}
        defaultValue={item.lastName}
      />
    </div>
  ));

  return (
    <>
      {list}
      <button
        type="button"
        //  appendの引数も型定義されている
        onClick={() => append({ firstName: "tanaka", lastName: "saburo" })}
      >
        add at last
      </button>
    </>
  );
};

export default ReactHookFormPart;

コード全体が見たい方はこちら
https://codesandbox.io/s/exciting-chatterjee-n68j3?file=/src/components/ReactHookFormPart.tsx

子コンポーネントでhooksの利用時に方を指定する必要がありますが、型定義済みの入力値を取得できます。
配列を操作する関数にも型定義がされているため、要素を追加する際にオブジェクトの構造を間違えてしまうこともなさそうです。

しかし、registerの使い方やuseForm、useFormContext、useFieldArrayのhook3段活用は、正直理解するのに時間がかかりました(使い方が正しいのかちょっと自信がありません...)。
全体的に簡単そうに見えて意外と難しい、というのがreact-hook-formの印象です。

終わり

formik
難しそう... → 意外とカンタンかも → typescriptがerrorとかanyとか

react-hook-form
なんかオシャレ → なんか難しい → なんか難しい(けどtypescriptエラーはない)

という印象でした。個人的にはtypescriptで書けることを重視しているのでひとまずreact-hook-formを使おうかと考えています。
ただ、formikもシンプルなフォームであればシンプルに書けるし、FieldArrayもrenderなしで書けるようにアップデートされて上記の悩みがなくなっていくかもしれません。
そもそもformikもreact-hook-formも詳しいわけではないので、もっと良い書き方があれば教えていただけると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.js で SVGファイルのサイズや色をCSSで変える方法

前提

  • React.js, webpack
  • ReactでSVGファイルをimgとして読むとスタイルをCSSで変更できない(サイズはできるが色はできない)のでinlineで読む必要がある
  • 複数のサイズや色のために複数のSVGファイルを用意したくない
  • SVGファイルを変換したReact Componentのファイルも作りたくない

概要

  • inline SVGとして埋め込む方法3つ
  • CSSでスタイルを上書きする方法

を紹介
利点としてはSVGをReact Component化せずそのまま使うことで、(デザイナーの用意した)素材を極力そのままにし、ファイル数も増やさずに色を変えることができる

SVGの埋め込み方

1. raw-loader + dangerouslySetInnerHTML の場合

webpack.config.js
        test: /\.svg$/i,
        loader: 'raw-loader',
jsx
import Hoge from '... .svg'
...
<div dangerouslySetInnerHTML={{ __html: Hoge }} />

typescriptならパスを通したglobal型置き場に次のように書く

global.d.ts
declare module '*.svg' {
  const content: string;
  export default content;
}

利点

  • 変換も何もされないので詰まったり失敗しない

欠点

  • dangerously

2. react-svg-loader の場合

https://github.com/boopathi/react-svg-loader/tree/master/packages/react-svg-loader を使う

webpack.config.js
      {
        test: /\.svg$/,
        use: [
          "babel-loader",
          {
            loader: 'react-svg-loader',
            options: {
              svgo: {
                plugins: [
                  { removeViewBox: false }, // to enable overwriteing width/height by CSS
                  { moveElemsAttrsToGroup: false }, // to prevent attribute destruction for overwriting color by CSS
                ],
                floatPrecision: 2,
              },
            },
          },
        ],
      },

ここでsvgoのオプションをdisableしているのは自分の扱うsvgが壊れないように設定した項目だが、扱うsvgによって別のdisableが必要かもしれない

jsx
import Hoge from '... .svg'
...
<Hoge />

typescriptならパスを通したglobal型置き場に次のように書く

global.d.ts
declare module '*.svg' {
  const content: React.ComponentType;
  export default content;
}

利点

  • 巨大ツールでない
  • 噛ませるbabel-loaderを自由に指定できる
    • next.config.js の defaultLoaders.babel とか

欠点

  • 最近メンテされてないっぽくてリリースがない
  • svgoを無効化できない
    • SVGの中身によっては更にsvgoのオプションをdisableしないとスタイルを上手く上書きできないかもしれない

3. svgr の場合

https://github.com/gregberge/svgr を使う以外のコードの部分はreact-svg-loaderと同じなので差分だけ書く

  1. https://react-svgr.com/docs/webpack/ に従う
  2. svgr.config.js を設定する
svgr.config.js
module.exports = {
  svgoConfig: {
    plugins: {
      removeViewBox: false, // to enable overwriteing width/height by CSS
      moveElemsAttrsToGroup: false, // to prevent attribute destruction for overwriting color
    },
  },
};

最適化不要、またはファイルに直接かけておく方針なら、svgoを無効化してしまう

svgr.config.js
module.exports = {
  svgo: false,
};

利点

  • メンテ・リリースが続いている
  • svgoを無効化できる
    • 破壊的変更のせいでうまくいかなかったら無効化で逃げれる
  • star多し
  • webpack以外にも対応

欠点

  • 依存ライブラリがちょっと多そう

スタイルの当て方

埋め込んだSVGやそのラッパーdivに対しCSSで以下のように書けばサイズや色を変えれる

    svg {
      width: 70px;
      height: 70px;
    }
    svg * :not([stroke='none' i]) {
      stroke: red;
    }
    svg * :not([fill='none' i]) {
      fill: red;
    }

ポイントは、SVGの色指定はstrokeとfillがあるので、それらがnoneでない箇所を上書きする点

所感

  • サクッと実現するならraw-loader + dangerouslySetInnerHTML
  • キレイにimportして埋め込みたいなら svgr
  • 2つのツールがデフォルトで使うsvgoのデフォルト設定がSVGを破壊的に変更するので曲者
  • @svgr/cli などを使ってSVG用のReact Componentを作ってPropsで柔軟に中身を変えるのもアリだけど、SVG素材とReact Componentでほぼ重複するのも悲しいし、用意された素材との同期などが少し面倒

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cognitoで学ぶ認証・認可 in AWS

この記事について

Webアプリのアクセス制御を行いたい!となったときに学ぶべきなのは認証・認可の仕組みです。
AWSにはAmazon Cognitoというユーザー管理を行うための仕組みが存在し、これを利用すれば「実装するだけなら」簡単にアプリのアクセス制御を行うことができます。

この記事では「Cognitoが実際に何をやってくれているのか?」というところまで掘り下げながら、簡単なReactアプリを作っていきます。

アジェンダ

  1. Cognitoのユーザープールを作って触ってみる
  2. Reactアプリに認証の仕組みを入れてみる
  3. Cognitoで認証済みの人だけが叩けるAPIをLambda + API Gatewayで作る
  4. CognitoのIDプールを作り、AWSでの認可の仕組みを学ぶ
  5. Cognito IDプールで認可された人だけが叩けるAPIをLambda + API Gatewayで作る

使用する環境・バージョン

  • React: 16.13.1
  • aws-amplify: 3.3.1
  • aws-amplify-react: 4.2.5
  • @aws-amplify/ui-react: 0.2.21

読者に要求する前提知識

  • Reactについての知識(本記事ではReactそのものの説明についてはしません)
  • REST APIが何かがわかること
  • (Lambda + API GatewayでのREST APIの作り方がわかること)
  • IAMロールが何かがわかり、自分で作成できること

Cognitoユーザープールの作成

まずは、AWSウェブコンソールからユーザープールを作成してみましょう。

ユーザープール作成

スクリーンショット 2020-09-19 19.48.19.png
任意のプール名をつけたあと、設定に進みます。今回は「ステップにしたがって設定」を選択します。
スクリーンショット 2020-09-19 19.56.21.png
認証(SignIn)時に要求する情報を「(ユーザーが決めた)ユーザー名」にするか「メールアドレスor電話番号」にするかの選択です。
今回は、メールアドレスのみを使用するようにしました。
スクリーンショット 2020-09-19 19.57.11.png
ユーザー作成(SignUp)時に、ユーザーが登録する必須項目を選択します。
今回の場合、メールアドレスの他に「name」を登録してもらうことにしました。
スクリーンショット 2020-09-19 20.00.14.png
パスワード関連の設定を行います。

  • パスワードの強度を設定: 数字・特殊文字・大文字小文字を要求するか・長さを決めます。
  • 自己サインアップ: ユーザー作成をAWSの管理画面からのみにするか、一般ユーザーもアプリ画面等から可能にするかを決めます。
  • パスワード有効期限: Sign Up時に発行される、初回ログインのための仮パスワードの有効期限を設定します。

スクリーンショット 2020-09-19 20.06.59.png
ここでは以下の設定を行います。

  • MFA(多要素認証)の設定: offにするかonにするかを選択します。今回は簡略化のためなし。
  • パスワード忘れの時の回復手段: ユーザーがパスワードを忘れたときには、登録済みのメールアドレスから回復させるようにした。
  • ロールの提供: Sign Up時やパスワード忘れからの復旧のときに、Cognitoがメールを送るためのロールを付与する。

他の設定は全てデフォルトのままにして、プールを作成します。
スクリーンショット 2020-09-19 21.39.12.png
ここで確認できるプールIDは控えておきましょう。

参考:[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました!

アプリクライアントの作成

ユーザープールを使う方のアプリの登録・ID払い出しを行います。
アプリを作る際に「クライアントシークレットを作る」のチェックを外しておきましょう。

スクリーンショット 2020-09-19 23.11.34.png

  • 有効なIDプロバイダ: すべて選択をチェック
  • コールバックURL: httpsかlocalhostを選択。ここでは暫定的にamazonを指定しておく。
  • 許可されているOAuthフロー: Authorization code grantとImplicit grantを選択
  • 許可されているOAuthスコープ: 全て選択

ここで作ったアプリクライアントIDもいずれ使うので控えておきます。

Amazon Cognitoドメインを設定

アプリの統合→ドメイン名のタブを開くと、以下のような画面になる。
スクリーンショット 2020-09-19 22.52.51.png
Amazon Cognitoドメインは、1つのリージョンの中で一意なドメインを設定する必要があります。
まだ使われていなさそうなドメインを「ドメインのプレフィックス」に入力・設定を保存します。

動かせるか確認

まずは、「ユーザーとグループ」のタブから、ユーザーを一つ作成しておきます。

OAuthフロー"Implicit Grant"の使用

アプリクライアントの作成のところで、"Implicit Grant"を許可しているので、まずはそれを試してみます。
以下のURLにアクセスします。

https://[amazon cognitoドメイン]/login?response_type=token&client_id=[アプリクライアントID]&redirect_uri=[コールバックURL]

すると以下のように、Cognitoが提供するサインインフォームが表示されます。
スクリーンショット 2020-09-25 23.53.39.png
ここで、先ほど作ったユーザーのメールアドレスとパスワードを入力してSign Inすると、コールバックURLにリダイレクトします。
この時のリダイレクト先URLハッシュの中に「IDトークン」「アクセストークン」が格納されています。

https://[コールバックURL]#id_token={IDトークン}&access_token={アクセストークン}&expires_in=3600&token_type=Bearer

この後の流れとしては、このURLハッシュで得られたIDトークンとアクセストークンをフロントエンド側で取得・保存し、アプリ内で利用することになります。

参考:【AWS】これだけ見れば理解できるCognito〜認証機能つきサーバレスアーキテクチャの作成〜

OAuthフロー"Authorization Code Grant"の使用

もう一つのOAuthフロー"Authorization Code Grant"も試してみます。
以下のURLにアクセスします。

https://[amazon cognitoドメイン]/login?response_type=code&client_id=[アプリクライアントID]&redirect_uri=[コールバックURL]

先ほどの"Implicit Grant"の違いはresponse_typeが"code"か"token"かというところです。

アクセスしたら、先ほど同様にCognitoが用意したログインフォームがあるので、同様に作ったユーザーでログインします。
すると、以下のURLにリダイレクトされます。

https://[コールバックURL]?code={認可トークン}

これも、リダイレクトURLで得られた認可トークンを保存して使用していくことになります。

今は何をしていたのか

今試してみた「Cognitoが用意したUIを使う」方法は、以下のAWS Blackbelt Seminor資料における「Cognito Auth API と Hosted UI を利用」に該当します。
スクリーンショット 2020-09-29 0.11.34.png
画像出典・資料:[AWS Black Belt Online Seminar] Amazon Cognito

「ログイン・サインインフォームのUIも自分で作りたい!」という場合や、「リダイレクトURLから得たトークンや認可コードを自分で保存・管理しておくのがめんどくさい!」という場合は、もう一方の「Cognito Identity Provider API を利用」を選択することになります。

Reactでフロントエンドを作り、Cognito認証と連携させる

「Cognito Identity Provider API を利用」した方法がこちらです。

ここからは、Cognitoでユーザー認証をしたユーザーだけがアクセス・中身を見ることができるウェブページをReactで作成していきます。

Reactアプリの枠組みを作成

create-react-appで簡単にアプリフレームが作れるので実行します。

$ npx create-react-app [appname]

これで、[appname]という名前のディレクトリができて、そこにReactアプリのフレームが出来上がっています。

パッケージのインストール

Cognitoでの認証・認可をコードベースで扱うためのパッケージをインストールします。
かつては"Amazon Cognito Identity SDK for JavaScript"等のSDKを使用していましたが、今は"AWS Amplify"の方のパッケージにCognito周りのパッケージが統合され、そちらがかなり便利なので今回はこれを使います。
公式Doc: Amplify Framework Documentation

$ npm install aws-amplify aws-amplify-react @aws-amplify/ui-react

コード作成

まずはindex.jsを以下のように修正します。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
+ import { Amplify } from 'aws-amplify';
+ 
+ Amplify.configure({
+   Auth: {
+       region: "Cognitoユーザープールを作ったリージョン",
+       userPoolId: "作成したユーザープールID",
+       userPoolWebClientId: "作成したアプリクライアントID"
+   }
+ });

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  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();

Amplify.configure()メソッドを使って、ReactアプリでどのCognitoプールを利用するかを指定しています。
参考:公式Doc: Amplify Doc AUTHENTICATION Create or re-use existing backend

次に、App.js を修正します。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
+ import { withAuthenticator } from '@aws-amplify/ui-react';

function App() {
  // (略)
}

- export default App;
+ export default withAuthenticator(App);

withAuthenticator()関数はhigher-order component (HoC)という、Reactコンポーネントを受け取って別のReactコンポーネントを返すものです。
withAuthenticator(引数)で得られるコンポーネントを利用することで、引数で渡したコンポーネントの閲覧にCognitoユーザー認証を要求するようにできます。

今回の場合はAppコンポーネントを引数に渡しているので、アプリのどこの画面を見るにしてもCognito認証が必要です。
参考:公式Doc: Amplify Doc AUTHENTICATION Authenticator

アプリの起動

npm run starthttp://localhost:3000が立ち上がり、アプリの挙動が確かめられるようになります。

http://localhost:3000にブラウザでアクセスしてみると、ReactのHello, Worldは表れず、以下のような認証画面が表れます。withAuthenticator(App)の機能通り、ここでユーザー認証が行われないと画面を閲覧することはできないようになっています。

スクリーンショット 2020-09-28 16.24.10.png

ここで、先ほどCognitoユーザープール管理画面で作ったユーザー情報を入力します。
すると、Appコンポーネントの中身である、ReactのHello, World!画面が無事に表れます。

スクリーンショット 2020-09-28 16.24.51.png

画面カスタマイズ

Sign IN/Up周りのUIをデフォルトからカスタマイズする方法を一部紹介します。

サインアウトボタンをつける

デフォルトではアプリ画面にログアウトボタンが存在しません。SAOみたいな狂気の世界にするつもりはない(はず)なので、ログアウトボタンをつけましょう。

AmplifyのAmplifySignOutコンポーネントが、ログアウトボタン機能を実装しているので、それを追加します。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator } from '@aws-amplify/ui-react';
+ import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        // (略)
+    <AmplifySignOut />
      </header>
    </div>
  );
}

export default withAuthenticator(App);

認証フォームをカスタマイズする

デフォルトでは、Sign Upフォームは「メールアドレス」「パスワード」「電話番号」です。しかし例えば、電話番号をフォームから抜きたいというときはどうしたらいいでしょうか。

認証フォームをwithAuthenticator()メソッドで導入した場合は、楽な代わりに細かいカスタマイズが難しいという側面があります。
実は、withAuthenticator()というのは、AmplifyAuthenticatorコンポーネントをラップしたものです。なので、withAuthenticator(App)とするのは以下のように書くのと同じです。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';
+ import { AmplifyAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
+      <AmplifyAuthenticator>
        <header className="App-header">
          // (略)
       <AmplifySignOut />
        </header>
+      </AmplifyAuthenticator>
    </div>
  );
}

- export default withAuthenticator(App);
+ export default App;

このとき、AmplifyAuthenticatorコンポーネントのブロックの中で、カスタマイズした認証フォームコンポーネントを設置すれば、デフォルト表示からフォームを変えることができます。

例えば、SignUpフォームを「メールアドレス」「パスワード」だけにするときは、Sign Upフォームを作るAmplifySignUpコンポーネントに適切なPropsを追加してやります。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';
+ import { AmplifyAuthenticator, AmplifySignOut, AmplifySignUp } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
      <AmplifyAuthenticator>
+      <AmplifySignUp
+       slot="sign-up"
+       usernameAlias="email"
+       formFields={[
+         {
+           type: "email",
+           required: true,
+         },
+         {
+           type: "password",
+           required: false,
+         },
+       ]} 
+      />
        <header className="App-header">
          // (略)
       <AmplifySignOut />
        </header>
      </AmplifyAuthenticator>
    </div>
  );
}

export default App;

こうすることで、AmplifyAuthenticatorコンポーネントのデフォルト表示が上書きされて、自分で書いたAmplifySignUpコンポーネントが使われます。

どんなコンポーネントが存在して、そんなPropsがあるのかについては、以下の公式ドキュメントを参照ください。
参考:公式Doc:Amplify Doc AUTHENTICATION Authenticator #Component

ユーザープールで得られる実態を確認

ここで、ブラウザのローカルストレージを確認してみます。
スクリーンショット 2020-09-28 16.38.32.png

  • idToken: ユーザープールに格納されている情報を確認するのに必要なJWTトークン
  • accessToken: ユーザープールの情報の更新のために必要なJWTトークン
  • refreshToken: idTokenやaccessTokenの有効期限が切れた時に、新しいものを取得するために必要なトークン
  • LastAuthUser: 現在ログインしている(=最後にログインした)ユーザーの、ユーザープール上でのID
  • userData: ユーザープールに登録されている情報

参考:Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する

Amplifyのコードを使っての認証に成功した暁には、これらの重要データが自動でローカルストレージに格納されています。Cognitoが用意したUIを使用した場合とは違い、自力でのトークン保存処理の実装が必要ありません。便利です。

また、idTokenやaccessTokenの有効期限が切れた場合(デフォルトで1時間)、refreshTokenというものを使ってトークンの更新が必要なのですが、それもAmplifyを使っている場合、開発者が意識しなくても自動で更新が行われています。これもAmplifyを使用するメリットの一つです。

これらの「Amplifyが裏でうまいことやってくれている仕組み」については、以下の記事で非常に詳しく説明されていますので、気になる方はご覧ください。
参考:AmplifyでCognitoのHosted UIを利用した認証を最低限の実装で動かしてみて動作を理解する

Cognitoで認証した人だけが叩けるAPIを作って連携する

Cognito認証をするアプリのバックエンドで使うAPIは、認証済みの人だけが叩けるようにしないと困ります。
そのため、「Cognitoでのユーザー認証が適切になされている人のみが叩けるAPI」を作ります。

Lambda関数を作る

API GatewayからGETリクエストを受け取ったら、Secret API!というbodyを含んだjsonを返す関数をLambdaで作ります。

package main

import (
    "encoding/json"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
    Body string `body`
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    res := Response{Body: "Secret API!"}
    jsonBytes, _ := json.Marshal(res)

    return events.APIGatewayProxyResponse{
        Body:       string(jsonBytes),
        Headers:    map[string]string{"Access-Control-Allow-Origin": "*"},
        StatusCode: 200,
    }, nil
}

func main() {
    lambda.Start(handler)
}

コードについての解説は、以下の過去記事をご覧ください。
AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた

注:Headersの{"Access-Control-Allow-Origin": "*"}は、CORSを有効にするために必要になります。

API Gatewayの設定

GETリクエストとLambda関数の連携

API Gatewayで、GETリクエストで上で作ったLambda関数を呼び出すように設定します(やり方は上記同様に、過去記事を参照ください)。
AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた

オーソライザの設定

ここで、APIのオーソライザの設定を行います。
スクリーンショット 2020-09-23 15.56.28.png
以下の通りに設定して作成します。

  • 名前: 任意の名前
  • タイプ: Cognitoを利用
  • Cognitoユーザープール: 先ほど作ったユーザープールの名前を選択
  • トークンのソース: Authrizationを入力

こうすることで、HTTPリクエストのヘッダにAuthrization=IDトークンをつけているものだけがこのAPIを叩けるようになります。
(Authrizationに有効なIDトークンが付いている=Cognitoでの認証済みとAPI Gatewayの方で判断するようになります)

CORSの設定

localhostから、別ドメイン(独自ドメインを割り当てないなら通常https://[文字列].execute-api.ap-northeast-1.amazonaws.comとなる)であるAPIを叩くので、CORS対策が必要になります。

アクションから、「CROSの有効化」を選択します。
スクリーンショット 2020-09-28 23.04.46.png
この設定のままで、CORSを有効化します。

参考:【AWS】これだけ見れば理解できるCognito〜認証機能つきサーバレスアーキテクチャの作成〜

APIのデプロイ→テスト

この状態でデプロイしたAPIをターミナルからcurlで叩くと、権限なしエラーが返ってきます。
(デプロイの仕方は過去記事参照→AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた)

$ curl [APIURL]
{"message":"Unauthorized"}

これで、Cognitoの認証なしにはAPIが叩けなくなっているということが確認できました。

フロントエンドでAPIを呼び出す

Cognitoで認証して閲覧しているアプリから、このAPIが叩けるかどうかをテストしてみましょう。
Cognito周りのメソッドだけでなく、APIを叩くためのメソッドもAmplifyに存在するので、それを利用します。
参考:Amplify公式Doc API(REST)

まずは、index.js内のAmplify.configure()メソッドで、アプリ内で使うAPIを指定します。

index.js
Amplify.configure({
  Auth: {
      // (略)
  },
+ API: {
+   endpoints: [
+     {
+         name: "TestAPI",
+         endpoint: "APIのエンドポイントURL",
+         region: "APIが公開されているリージョン",
+     }
+   ]
+ }
});

次に、App.jsを書き換えて、APIから受け取った文字列をアラートで表示するボタンを設置します。

App.js
function App() {
  return (
    <div className="App">
      <AmplifyAuthenticator>
      // (略)
      <header className="App-header">
        // (略)
+        <div class='container'>
+         <button id='button' onClick={() => showSecret()}>Click Me!</button>
+        </div>
        // (略)
      </header>
      </AmplifyAuthenticator>
    </div>
  );
}

ボタンをクリックした時に実行される関数showSecret()も、以下のようにApp.js内に実装します。

App.js
import { Auth, API } from 'aws-amplify';

const showSecret = async function() {
    const apiName = 'TestAPI';  // index.jsで指定したnameと同じ名前にする
    const path = ''; 
    const myInit = { 
      headers: { 
        Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`,
      },
    };

    API.get(apiName, path, myInit)
    .then(response => {
      console.log(response);
      alert(response.Body);
    })
    .catch(err => {
      console.log(err);
      alert(err);
    });

  };

Authorizationヘッダに、IDトークンを付与するように設定しています。

この状態でアプリを動かして、新たに作成したボタンを押すことで、APIからのレスポンスをアラートに表示させることができます。

IDプールの作成

次に、ユーザープールに続いて、Cognitoの別機能であるIDプールを使っていきます。

なぜIDプールを作成するのか

ユーザープールだけでもログインフォームが作れたのに、どうしてIDプールも作るの?ということを説明します。

Cognitoのユーザープールで行えるのは「認証」のみで「認可」は行うことができません。
例えば、「このリクエストを送っているのは、メールアドレスhttp://example.comで登録したAさんだな」ということを保証することはできても、「そのAさんにDBアクセスをやらせていいよ」と許可することはできないのです。

なので、認証したユーザーにDynamoDB等の「AWSリソースにアクセスさせるために」「IAMロールを割り当てる」という「認可」の作業を行うためには、IDプールが必要になるのです。
(逆に言えば、IAMロールを絡ませる必要が全くないアプリケーションの場合、ユーザープールだけで事足ります。)

作成手順

スクリーンショット 2020-09-28 21.34.03.png
IDプール名に好きな名前をつけます。
認証されていないIDへのアクセス・認証フローの設定にはどちらにもチェックはつけずに、「認証プロバイダー」のタブを開きます。

スクリーンショット 2020-09-21 1.18.11.png
ここで、先ほど作ったユーザープールIDとアプリクライアントIDを入力して、次に進みます。

スクリーンショット 2020-09-28 21.35.03.png
ここで、認証されたユーザー・認証されなかったユーザーにどんなロールを与えるのかを設定する画面です。今回はそれぞれに対して「Auth_Role」「UnAuth_Role」を与えるように新しくロールを新規作成しました。

ここまでの設定が済んだら、IDプールの作成は完了です。
作成完了したら、「IDプールの編集」タブから、プールのIDを確認して控えておきましょう。

Reactアプリとの連携

IDプールを作成できたら、それを先ほどのReactアプリで使えるようにしましょう。

index.js内のAmplify.configure()メソッドの中身を以下のように編集します。

index.js
Amplify.configure({
   Auth: {
       region: "Cognitoユーザープールを作ったリージョン",
       userPoolId: "作成したユーザープールID",
       userPoolWebClientId: "作成したアプリクライアントID"
+      identityPoolId: "(先ほど控えた)IDプールのプールID",
   }
});

この状態でアプリを起動することで、ログインユーザー(ユーザープールのユーザー1人)ごとに、IDプールのIDが割り振られる&ログインユーザーが持つIAMロールが与えられるようになります。

IDプールから得られるものの実態を確認する

「IDプールのIDが与えられる」「IAMロールが与えられる」といっても、アプリの見た目上は何も変わらないので、「どこがどうなったの??」と思う方もいるでしょう。
ここからは、IDプールと連携したことで何を得られたのか、実態を確認してみましょう。

まずはコンソールでものを確認

App.jsの中でcurrentCredentials()というメソッドを実行して、結果をみてみましょう。

App.js
// (略)
import { Auth } from 'aws-amplify';

function App() {
+  Auth.currentCredentials().then(console.log);
   // (略)
}

export default App;

この状態でアプリを起動すると、コンソール上でcurrentCredentials()で得られたデータを確認できます。
スクリーンショット 2020-09-28 19.48.09.png
補足:このcurrentCredentials()メソッドは、IDプール未連携時にはエラーが返ってきます。

ここで得られる「accessKeyId」と「secretAccessKey」が、IDプールから得ることができる"AWS Security Credentials"というものの実態です。
また今回の場合、Security Credentialsの中でも"AWS Temporary Security Credentials"というものが利用されます。なので「sessionToken」も重要な役割を果たします。

"AWS Security Credentials"とは?

「accessKeyId」と「secretAccessKey」の組み合わせのことです。

AWSでは、正しいアクセスキーIDとシークレットアクセスキー(=正しいAWS Security Credentials)を持っていることで、特定のAWSアカウントのあるIAMロールを持ったユーザーであると判定しています。

わかりやすい例としては、普段ターミナルでAWS CLIを叩く方だと、~/.aws/credentialsの設定ファイルに普段使用するアクセスキーIDとシークレットアクセスキーを保存しているかと思います。これは、ターミナルからコマンドを実行している人が、正当なIAM UserかどうかをAWS Security Credentialsで判断している例です。

AWS CLIでのAWS Security Credentialsは、ユーザーがAWSコンソールで更新作業を行わない限りずっと有効であり、いわゆるPermanentなものです。
それに対して、今回のような「ユーザーがアプリにログインしている間だけ使える」ようなものを"AWS Temporary Security Credentials"と呼びます。

一般的にPermanentな方のAWS Security CredentialsはIAM Userに、Temporaryな方のAWS Security CredentialsはIAM Roleに紐づいていることが多いです。

AWS Security Credentialsの使い方

ここからは、Security Credentialsを我々はどのように使うのかを見ていきます。

まずは、「accessKeyId」と「secretAccessKey」を使って(=Security Credentialsを使って)利用するAWSサービスというのは、たいていREST APIになっています(AWS APIという)。
AWS APIはリクエストがあるたびに、「このリクエスト主が正当な権限の持ち主かどうか?」というのをリクエスト内容から判断する必要があります。

AWS APIが権限を確認できるように、ユーザーはhttpリクエストヘッダのAuthorizationフィールドに「Signature V4」という方式の署名を付与する必要があります。
その「Signature V4」署名の生成に必要なのが「accessKeyId」と「secretAccessKey」です。

そして、AWS Temporary Security Credentialsを使用する場合は、「権限が期限切れじゃないかどうか」ということを判断するために「sessionID」が使われます(リクエストヘッダのX-Amz-Security-Tokenフィールドに埋め込む)。

まとめ

今回の場合、IDプールから得られるSecurity Credentialsを利用することで、認証されたアプリの利用者がIAMロールを使えるようになるのです。

参考:【AWS】AWS APIの認証・認可の仕組みを理解する【Signature V4】
参考:【Amplify】APIのAuthorization方式「AWS_IAM」を理解する#1 ~解説編~【AWS】

ユーザープールのグループを使ってユーザーごとに権限を分ける

IDプールと連携することで、「認証済みユーザーにはIAMロール"Auth_Role"」「認証されていないユーザーにはIAMロール"UnAuth_Role"」が渡されるように設定することができました。
しかし、認証済みユーザーに割り当てられるIAMロールが1つというのは少々窮屈です。例えば、認証済みユーザーの中でも、「一般ユーザー」と「管理者ユーザー」で違うロールを与えたいというケースはよくあるものでしょう。

今回は、Cognitoユーザープールの「ユーザーグループ」という機能を用いることで、認証済みユーザーにそれぞれ適切なロールを分けて与えられるように設定し直してみようと思います。

参考:AWS CLIで動かして学ぶCognito IDプールを利用したAWSの一時クレデンシャルキー発行

ユーザーグループの作成

スクリーンショット 2020-09-28 22.04.06.png
「ユーザーとグループ」からグループの作成を選択します。すると、以下のようなグループ作成画面が現れます。
スクリーンショット 2020-09-28 22.04.20.png
ここで任意のグループ名をつけます。まずは管理者ユーザー用のグループを作るため、それっぽい名前をつけましょう。
IAMロールのところで、新しく「AdminAuth_Role」という名前(ポリシーはAuth_Roleと一旦同じものにしておく)を作成し、それを選択します。

同様に、一般ユーザー用のグループ(IAMロールはAuth_Role)も作成しておきます。

ユーザーをグループに追加

ユーザーグループを作成したら、ユーザーをそこに所属させます。

ユーザー一覧からユーザーを選択して、「グループに追加」を選択することで可能です。
スクリーンショット 2020-09-28 22.05.11.png

IDプールの設定

先ほど作ったIDプールの管理画面から、「IDプールの編集」を開きます。

編集画面の中から、「認証プロバイダー」というタブを開きます。
スクリーンショット 2020-09-28 20.06.26.png
認証されたロールの選択というところで、「トークンからロールを選択する」と設定します。

これで、ユーザーグループごとに設定されたロールを与えるように設定されました。

APIのアクセス許可をIAMロールで行う

せっかくユーザーごとに違うロールを与えられるようになったので、何かそれを活かした仕組みを作ってみたくなります。

ここでは、管理者ユーザーのロールを持った人にだけ叩けるAPIを作ってみようと思います。

ロールの設定

管理者ユーザーのロールポリシー(AdminAuth_Role)を以下のように編集します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*",
+               "execute-api:Invoke"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

"execute-api:Invoke"を追加することで、API Gatewayで公開されているAPIを叩くための権限を付けています。

API Gatewayでの設定

API Gatewayで、先ほど作ったAPIのGETメソッドリクエストの設定欄を開きます。
スクリーンショット 2020-09-28 20.33.31.png
「認可」のところを「AWS_IAM」に設定します。
こうすることで、"execute-api:Invoke"のポリシーがついたロールを持ったユーザー以外のリクエストを弾くようにできます。

コードの準備

前述した通り、ユーザーがIAMロールを持っていることを伝えるためには、適切なヘッダをつけたhttpリクエストを送る必要があります。
しかし、Amplifyを使えばこの面倒なヘッダ設定を全部自動でやってもらえます。

まずはコードを直します。
APIの認証が「Cognitoユーザープールでの認証の有無」ではなくなったため、AuthorizationにIDトークンをつける必要は無くなりました。なので、その記述を削除します。

App.js
- const showSecret = async function() {
+ const showSecret = function() {

    const apiName = 'TestAPI';  // index.jsで指定したnameと同じ名前にする
    const path = ''; 
-   const myInit = { 
-      headers: { 
-        Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`,
-      },
-   };


    // getメソッドを叩いている
-    API.get(apiName, path, myInit)
+    API.get(apiName, path)
      .then(response => {
        alert(response.data);
      })
      .catch(err => {
        alert(err);
      });

  };

コード上では、AWS APIを利用するためのSignature V4署名やsessionID付与を行なっていません。
しかし、AmplifyのAPI.get()メソッドを使うことで、それらの前処理を全てAmplifyに任せるようになっているので、上のコードで問題なく動きます。

実際に、上記のコードでAPIを叩いたときのhttpリクエストをディベロッパーツールで見てみると、確かにAuthorizationヘッダにSignature V4で生成した署名を、X-Amz-Security-Tokenの部分にsessionTokenが自動で埋め込まれている様子が確認できます。
スクリーンショット 2020-09-28 22.30.06.png

補足:Signature V4での署名を付与する際のAuthorizationヘッダの仕様については、次のAWS公式ドキュメントを参照ください。→タスク 4: HTTP リクエストに署名を追加する

まとめ

本記事の内容はこれで全てです。

Cognitoを触ることを通して、Cognitoの使い方だけでなく、一般的な・AWSでの認証認可の知識も深まります。
ここで行なったことは基本的なことばかりだと思うので、あとは公式ドキュメントを読みながら色々いじってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】styled-componentsを導入から適応まで

 styled-componentsとは?

React内でCSSを適用させるための一つの手段です。

直感的で使いやすいので、今回はstyled-componentsまとめてみます。

 基本的な構文

import * as React from "react";
import styled from "styled-components";

const Title = styled.h1`
  color: blue;
`;

const Header = <Title>ブログ</Title>;

解説していきますね。

import styled from "styled-components";

上記でstyled-componentsの機能をimportします。

const Title = styled.h1`
  color: blue;
`;

上記で、<Title>ブログ</Title>をスタイリングしています。

下記に構文をまとめておきます。

const <要素名> = styled.<タグ名>`
  // ここにあてたいCSSを書く。
`;

 導入

 npm i styled-components @types/styled-components

npmでインストールします。僕の環境ではtypescriptを導入しているので、型定義も一緒にインストールしています。

あとは、最初の方に解説したような形でスタイリングできます。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながら graphql-code-generator の便利さを痛感する その2

はじめに

今日も今日とて、フロントReact + バックRailsの...

ということで、今回は今更ながら graphql-code-generator の便利さを痛感するの続きとして、登録、削除等のMutationの部分をやっていきます。

※構成は前回と同じです。

とりあえずApolloの公式に沿ってやってみる

まずはTODOを追加する仕組みを作っていきます。

追加用のテキストフィールドを用意しました。
ここに値を入力して、エンターで追加処理が実行される仕組みとします。

スクリーンショット 2020-09-22 12.44.46.png

src/App.tsx
+import { gql, useMutation } from "@apollo/client";

...

+const ADD_TODO = gql`
+  mutation addTodo($name: String!) {
+    addTodo(input: { name: $name }) {
+      todo {
+        id
+        name
+      }
+    }
+  }
+`;

 const App = () => {
   const { loading, data } = useTodosQuery();
+  const [addTodo] = useMutation(ADD_TODO);

...
         </p>
+        <input
+          type="text"
+          onKeyPress={(e) => {
+            if (e.key === "Enter") {
+              addTodo({ variables: { name: e.currentTarget.value } });
+            }
+          }}
+        />
         {loading ? (
...

文字を入力して、エンターで追加処理が実行できる仕組みができました。

エンター押下後、画面をリロードすると、TODOが追加されていることがわかります。

スクリーンショット 2020-09-22 14.18.50.png

リロードしないと動きがわからないので、ついでに、追加処理に加え、入力欄のクリア、一覧情報の再取得処理をやっておきます。

src/App.tsx
...
-  const { loading, data } = useTodosQuery();
+  const { loading, data, refetch } = useTodosQuery();
-  const [addTodo] = useMutation(ADD_TODO);
+  const [addTodo] = useMutation(ADD_TODO, {
+    update() {
+      refetch();
+    },
+  });

...

           onKeyPress={(e) => {
             if (e.key === "Enter") {
               addTodo({ variables: { name: e.currentTarget.value } });
+              e.currentTarget.value = "";
             }
           }}

特に型関連でエラーは起きませんでした。

もう少し便利になるよう作り込んでみる

何も入力せずに、登録処理を実行し、エラーが出るような仕組みを入れます。

Todoにバリデーションエラーのフィールドを追加して、以下のような値がレスポンスに含まれるようにしました。

"errors":[{"field":"name","error":"blank"}]

また、処理の結果が正常に行われたかのresultフィールド(Boolean)を追加しました。

resulttrueなら一覧をリロード、falseならエラー情報をalertで表示する仕組みを作ってみます。

src/App.tsx
 const ADD_TODO = gql`
   mutation addTodo($name: String!) {
     addTodo(input: { name: $name }) {
       todo {
         id
         name
+        errors {
+          field
+          error
+        }
      }
+      result
    }
  }
`;
src/App.tsx
   const [addTodo] = useMutation(ADD_TODO, {
-    update() {
-      refetch();
-    },
-  });
+    update(
+      _cache,
+      {
+        data: {
+          addTodo: {
+            todo: { errors },
+            result,
+          },
+        },
+      }
+    ) {
+      if (result) {
+        refetch();
+      } else {
+        errors.forEach(({ field, error }) => {
+          alert(`${field} ${error}`);
+        });
+      }
+    },
+  });

errorsの型が宣言されていないので、エラーが発生しました。

スクリーンショット 2020-09-22 16.10.28.png

型宣言をしてあげます。

type ValidationErrorType = {
  field: string;
  error: string;
};
-errors.forEach(({ field, error }) => {
+errors.forEach(({ field, error }: ValidationErrorType) => {
  alert(`${field} ${error}`);
});

エラーは解消され、バリデーションエラーメッセージがalertで確認できるようになりました。

スクリーンショット 2020-09-22 16.15.46.png

前回同様、graphql-code-generatorを使って、綺麗にしていこうと思います。

graphql-code-generatorを使う

まずは設定から

queriesとは別にmutations用のディレクトリを作成して、今回作成したクエリは、そちらに格納します。

codegen.yml
-documents: ./graphql/queries/*.graphql
+documents:
+  - ./graphql/mutations/*.graphql
+  - ./graphql/queries/*.graphql
graphql/mutations/add_todo.graphql
mutation addTodo($name: String!) {
  addTodo(input: { name: $name }) {
    todo {
      id
      name
      errors {
        field
        error
      }
    }
    result
  }
}

これでyarn generateを実行すると、src/types.d.tsにTODOを追加する用のuseAddTodoMutationが用意されました。

useAddTodoMutationを使って追加処理を書き直してみます。

-import { useTodosQuery } from "./types.d";
+import { useTodosQuery, useAddTodoMutation } from "./types.d";

+const [addTodo] = useMutation(ADD_TODO, {
+const [addTodo] = useAddTodoMutation({

ん?エラーが起きました。

スクリーンショット 2020-09-22 21.01.17.png

どうやらdatanullundefinedの可能性があるそうです。

それらを考慮して、少し書き直します。

const [addTodo] = useAddTodoMutation({
  update(_cache, { data }) {
    const result = data?.addTodo?.result || false;
    const errors = data?.addTodo?.todo.errors || [];

    if (result) {
      refetch();
    } else {
      errors.forEach((e) => {
        if (e) alert(`${e.field} ${e.error}`);
      });
    }
  },
});

少し手直しが必要でしたが、クエリと、型宣言の用意が不要になったので、ソース的にはスッキリしました :thumbsup:

同様に削除処理を実装してみる

雑ですが、各TODOの隣に、削除ボタンを用意して、削除処理を実行できるようにします。

スクリーンショット 2020-09-27 23.50.47.png

graphql/mutations/del_todo.graphql
mutation delTodo($id: ID!) {
  delTodo(input: { id: $id }) {
    todo {
      id
    }
  }
}

削除対象のTODOを特定するために、IDを取得する必要があります。

graphql/queries/todos.graphqlでは、TODOのnameだけ取得しているので、idもとるように修正します。

graphql/queries/todos.graphql
 query todos {
   todos {
+    id
     name
   }
 }

yarn generateを実行するとuseDelTodoMutation関数がsrc/types.d.tsに生まれました。

このuseDelTodoMutationを使って、削除ボタンクリック時に削除処理が実行できるようにします。

src/App.tsx
-import { useTodosQuery, useAddTodoMutation } from "./types.d";
+import { useTodosQuery, useAddTodoMutation, useDelTodoMutation } from "./types.d";


+const [delTodo] = useDelTodoMutation({
+  update() {
+    refetch();
+  }
+});


-{data && data.todos.map(({ name }, i) => <li key={i}>{name}</li>)}
+{data && data.todos.map(({ id, name }, i) => <li key={i}>{name}<button onClick={() => delTodo({ variables: { id } })}>削除</button></li>)}

お手軽に削除処理が実装できました :tada:

add-del.gif

最後に

一つの画面、コンポーネントで、一覧取得、追加処理、削除処理を実装したので、少しボリュームの大きめなファイルとなってしまったかもしれません。

別のファイルに型定義やクエリ情報を定義して、importでも良いかもしれないですが、人力で且つ、複数人となると、管理が次第に煩雑になったりする可能性があると思います。

これが、ある意味graphql-code-generatorのルールに則って開発をしていると、その問題が解消されるのではないかと思いました。

今回は、かなり小規模な例として、実装したので、導入にあたって障害となる箇所をあまり感じられませんでした。
LGTM なツールと思います :thumbsup:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

分かりすぎるVue.jsチートシート

自己紹介

むちゃんです。
関西を拠点に活動しているフロントエンドエンジニアです。

HAL大阪の2回生です:boy_tone2: (2020.9.29現在)

イベントなど回っているので是非大阪辺りの方は会いましょう!!

1...JSフレームワークについて

「Angular」,「React」,「Vue」この3個がJS人気フレームワークです。
今回はその中のVueについて詳しく話していきたいと思います
js.jpeg

Vueの公式ドキュメント

2...Vue.jsとは?

Vue.jsユーザーインターフェースを構築するためのJavaScriptのフレームワークです。
最近ではReactAngularVue.jsがJavaScriptの人気フレームワークとして定着してきています。
その中でもVue.jsは初心者に優しく比較的に学習しやすい事もあり現在の国内での人気は頭一つ抜けている印象があります。

Vue.jsの特徴

・コンポーネントの再利用性が高い
・学習コストが低く、取っ付きやすい
・拡張性が高い

3...Vue.jsで抑えとくべきポイント

①Vuex

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。
※貯蔵庫的な役割と思っていいです。

詳しいのは公式=>https://vuex.vuejs.org/ja/

Vuexの流れ(図解)

vuex.png

①componentでdispatchする
②actionsでAPIデータのやり取りする
③それをcommitしてmutationsを呼ぶ
④mutationsでデータの値変える
⑤するとstateの値が変わる

ざっくりした流れですが公式の補足程度で見てもらえると嬉しいです。

⭐️おすすめ
[初めてのアウトプット]
爆速理解!超簡単Vuex入門①


②基本的に使うオブジェクト構成

構造だけまとめているので詳しいことは公式で確認してください。

sample.js
ver vm = new Vue({
  el: 'app',
// ①使用するデータ
  data: {
    name: 'note',
    price: 500
  },
// ② 算出プロパティ
  computed: {
     isPrice: function () {
      return this.price / 2;
    }
  },
  // ③ 使用するメソッド
  methods: {
    isPrice: function() {
      return this.price / 2;
    }
  }
})

※thisについての記事

data(使用するデータ)

sample.js
var vm = new Vue ({
  el: '#app',
  data: {  //data: { プロパティ名: 値 }
    name: 'note',
    price: 500
  }
})
samole.html
<div id="app">
  <p>{{ name }}</p>
</div>

computed (算出プロパティ)

sample.js
var vm = new Vue ({
  el: '#app',
  data: {
    name: 'note',
    price: 500
},

  computed: {
    // isPriceを定義
    isPrice: function () {
      return this.price / 2;
    }
  }
})
sample.html
<div id="app">
  <!-- 250が表示される -->
  <p>{{ isPrice }}円</p>
</div>

methods(メソッド)

sample.js
var vm = new Vue ({
  el: '#app',
  data: {
    name: 'note',
    price: 500
},
  methods: {
    // メソッドisPriceを定義
    isPrice: function() {
      return this.price / 2;
    }
  }
})
sample.html
<div>
  <!-- ボタンをクリックするとisPrice関数が実行 -->
  <button @click="isPrice()">半額</button>
  <!-- 250円と表示 -->
  <p>{{ price }}円</p>
</div>

computedとmethodsの違い

算出プロパティとメソッドの使い分け

結論 & 資料

この辺りをある程度理解できればアウトプットに全力を注いでいいと思います。

⭕️ アウトプットやオススメの記事一覧

Vue.jsを100時間勉強して分かったこと
5分でわかるVue.js基礎の基礎
2019年版Vue.jsを使ってる人には必ず知っていてほしいVue.jsの武器
Vue-routerを使って、SPAをシンプルにはじめてみる
vueとexpressでSPAを作る

あとがき

今回は自分なりにVueについて簡潔にまとめてみした:thumbsup_tone2:

次回はReactについてまとめてみたいと思います。

Twitter @HomgMuchan ぜひフォロー待っています❗️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む