AWS AmplifyのReact向けチュートリアルをTypeScript化しながら体験してみた

## はじめに

今回は数あるAWSサービスの中でもフロントエンドエンジニアが扱うにとても便利なサービスであるAmplifyのチュートリアルを試してみるついでにTypeScript化しながら進めてみます。

## AWSとは

https://aws.amazon.com/jp/what-is-aws/

公式サイトでは下記のように説明されています。

> **AWS によるクラウドコンピューティング**
> アマゾン ウェブ サービス (AWS) は、世界で最も包括的で広く採用されているクラウドプラットフォームです。世界中のデータセンターから 200 以上のフル機能のサービスを提供しています。急成長しているスタートアップ、大企業、主要な政府機関など、何百万ものお客様が AWS を使用してコストを削減し、俊敏性を高め、イノベーションを加速させています。

Amazon Web Servicesの略称で、Amazon社が提供してるクラウドコンピューティングサービスの総称で、エンジニアなら知らない人はいないくらいのサービスだと思います。

同様のサービスとして、Google社のGoogle Cloud Platform ServiceやMicrosoft社のAzureがありますが、一番の老舗ということもありサービスの数は群を抜いて多いようです。

従来ですとサーバーを利用するには、物理的にサーバー本体を用意してデータセンターに配置する、足りなくなったら増やす、いらなくなったら外すといった時間と労力がかかる作業が必要でしたが、AWSを利用することでブラウザ上から必要な時に必要な分のサーバーを用意することができるようになりました。必要な時に必要な分だけでというなんともすばらしいサービスです。

AWSのサイトを見ると、ひとまずたくさんいろいろなサービスを展開しているというのはわかると思いますが、サービスが200以上もあるということでまさに雲をつかむような大きなイメージです。。

サービス一覧をまとめてくださっている記事があったので共有させていただきます。🙇

https://qiita.com/moritalous/items/31a56acbf2ce367b712d

さらに学習ロードマップも！

https://qiita.com/KurokawaKouhei/items/4e9aa3b526f3f233bf85

## AWS Amplifyとは

今回は数あるサービスの中でもフロントエンドエンジニアが扱うにとても便利なサービスであるAmplifyを試してみます。AWS Amplifyは、ひとつの機能を提供しているサービスというわけではなく、Webアプリケーションやモバイルアプリケーションに必要なサービスを複合的に構築するためのプラットフォームのようなものになります。APIや認証、CDN、ホスティングなどなどコマンドやWebコンソールから簡単に機能追加することができるおそろしく便利なサービスです。

GitHubと連携することでCI/CDまで対応しています。

フロントエンドエンジニアとしてはバックエンドにAmplify使えれば生きていけるのでは！？くらいのサービスなのではと個人的には感じています。

そんなAmplifyにも他のサービスと同じ用にいろいろな言語やフレームワークのチュートリアルがあるのですが、今回はReactのチュートリアルをTypeScriptに置き換えながら勉強してみました。

https://docs.amplify.aws/start/q/integration/react

## チュートリアル

TypeScriptに置き換えるといってもほとんどはチュートリアルのまま進めていくことであっという間に認証機能付きTodoアプリが出来上がってしまいます🙈

TypeScriptと後ほどHooks部分をReduxに置き換えたかったので、プロジェクトのテンプレートに`redux-typescript`を利用しました。

```bash
$ yarn create react-app project-name --template redux-typescript
```

他にチュートリアルの項目と若干異なるのはこの部分くらいでしょうか。

[Create a GraphQL API and database](https://docs.amplify.aws/start/getting-started/data-model/q/integration/react#create-a-graphql-api-and-database)

```bash
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target typescript // ここでTypeScriptを選ぶことで以後の質問が変わる
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts
```

Add authenticationまで反映した際のソースは下記のようになりました。
Amplifyの設定とたったこれだけでログイン認証からGraphQLとの連携までできてしまうのが恐ろしいところ。。。

```typescript:src/App.tsx
import React, { useEffect, useState } from 'react';
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import { withAuthenticator } from '@aws-amplify/ui-react';
import { createTodo } from 'src/graphql/mutations';
import { listTodos } from 'src/graphql/queries';

import { ListTodosQuery, CreateTodoInput } from 'src/API';

import awsExports from 'src/aws-exports';
import { GraphQLResult } from '@aws-amplify/api';
Amplify.configure(awsExports);

const initialState = { name: '', description: '' };

const App: React.VFC = () => {
  const [formState, setFormState] = useState(initialState);
  const [todos, setTodos] = useState<CreateTodoInput[]>([]);

  useEffect(() => {
    fetchTodos();
  }, []);

  const setInput = (key: string, value: string) => {
    setFormState({ ...formState, [key]: value });
  };

  const fetchTodos = async () => {
    try {
      const todoData = (await API.graphql(
        graphqlOperation(listTodos),
      )) as GraphQLResult<ListTodosQuery>;
      if (todoData.data?.listTodos?.items) {
        const todos = todoData.data.listTodos.items as CreateTodoInput[];
        setTodos(todos);
      }
    } catch (err) {
      console.log('error fetching todos');
    }
  };

  const addTodo = async () => {
    try {
      if (!formState.name || !formState.description) return;
      const todo: CreateTodoInput = { ...formState };
      setTodos([...todos, todo]);
      setFormState(initialState);
      (await API.graphql(
        graphqlOperation(createTodo, { input: todo }),
      )) as GraphQLResult<CreateTodoInput>;
    } catch (err) {
      console.log('error creating todo:', err);
    }
  };
  return (
    <div style={styles.container}>
      <h2>Amplify Todos</h2>
      <input
        onChange={(event) => setInput('name', event.target.value)}
        style={styles.input}
        value={formState.name}
        placeholder="Name"
      />
      <input
        onChange={(event) => setInput('description', event.target.value)}
        style={styles.input}
        value={formState.description}
        placeholder="Description"
      />
      <button style={styles.button} onClick={addTodo}>
        Create Todo
      </button>
      {todos.map((todo, index) => (
        <div key={todo.id ? todo.id : index} style={styles.todo}>
          <p style={styles.todoName}>{todo.name}</p>
          <p style={styles.todoDesctiption}>{todo.description}</p>
        </div>
      ))}
    </div>
  );
};

const styles: {
  [key: string]: React.CSSProperties;
} = {
  container: {
    width: 400,
    margin: '0 auto',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center',
    padding: 20,
  },
  todo: { marginBottom: 15 },
  input: { border: 'none', backgroundColor: '#ddd', marginBottom: 10, padding: 8, fontSize: 18 },
  todoName: { fontSize: 20, fontWeight: 'bold' },
  todoDescription: { marginBottom: 0 },
  button: {
    backgroundColor: 'black',
    color: 'white',
    outline: 'none',
    fontSize: 18,
    padding: '12px 0px',
  },
};

export default withAuthenticator(App);
```

## HooksをRedux Toolkitに置き換えてみる

次にReduxの学習を兼ねてHooksでの状態管理部分をRedux Toolkitへ置き換えてみました。
編集ファイル以外は`redux-typescript`テンプレートのままになります。

```typescript:src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import todoReducer from 'src/features/todo/todoSlice';

export const store = configureStore({
  reducer: {
    todo: todoReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;
```

```typescript:src/features/todo/todoAPI.ts
// チュートリアルでApp.tsxに記載したAPI接続部分のロジックを別ファイルに分離します
import Amplify, { API, graphqlOperation } from 'aws-amplify';
import { createTodo } from 'src/graphql/mutations';
import { listTodos } from 'src/graphql/queries';

import { ListTodosQuery, CreateTodoInput } from 'src/API';

import awsExports from 'src/aws-exports';
import { GraphQLResult } from '@aws-amplify/api';
Amplify.configure(awsExports);

export const fetchTodos = async () => {
  let todos;
  try {
    const todoData = (await API.graphql(
      graphqlOperation(listTodos),
    )) as GraphQLResult<ListTodosQuery>;
    if (todoData.data?.listTodos?.items) {
      todos = todoData.data.listTodos.items as CreateTodoInput[];
    }
  } catch (err) {
    console.log('error fetching todos');
  }

  return todos;
};

export const addTodo = async (formState: { name: string; description: string }) => {
  try {
    if (!formState.name || !formState.description) return;
    const todo: CreateTodoInput = { ...formState };
    (await API.graphql(
      graphqlOperation(createTodo, { input: todo }),
    )) as GraphQLResult<CreateTodoInput>;
  } catch (err) {
    console.log('error creating todo:', err);
  }
};
```

`redux-typescript`テンプレートのサンプルにある`CounterSlice`をベースに書き換えていきます。

```typescript:src/features/todo/todoSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'src/app/store';
import { CreateTodoInput } from 'src/API';
import { addTodo, fetchTodos } from './todoAPI';

export type todosState = {
  todos: CreateTodoInput[];
  status: 'idle' | 'loading' | 'failed';
};

const initialState: todosState = {
  todos: [],
  status: 'idle',
};

/**
 * Todo一覧の取得
 */

export const fetchAsyncTodos = createAsyncThunk('todo/fetchAsyncTodos', async (_, thunkApi) => {
  const todos = await fetchTodos().catch((error) => {
    throw error;
  });
  if (todos) {
    return todos;
  } else {
    return initialState.todos;
  }
});

/**
 * Todoの追加
 */

export const createAsyncTodo = createAsyncThunk(
  'todo/createAsyncTodo',
  async (formState: { name: string; description: string }) => {
    await addTodo(formState).catch((error) => {
      throw error;
    });
    return formState;
  },
);

export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchAsyncTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchAsyncTodos.fulfilled, (state, action) => {
        state.status = 'idle';
        const data = action.payload.map((post) => {
          return { name: post?.name, description: post.description };
        });
        state.todos = data;
      })
      .addCase(createAsyncTodo.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(createAsyncTodo.fulfilled, (state, action) => {
        state.status = 'idle';
        state.todos.push(action.payload);
      });
  },
});

export const selectTodos = (state: RootState) => state.todo.todos;

export default todoSlice.reducer;
```

App.tsxはかなりすっきりです。

```typescript:src/App.tsx
import React, { useEffect, useState } from 'react';
import { withAuthenticator } from '@aws-amplify/ui-react';
import { useAppDispatch, useAppSelector } from 'src/app/hooks';
import { selectTodos, fetchAsyncTodos, createAsyncTodo } from './features/todo/todoSlice';

const initialState = { name: '', description: '' };

const App: React.VFC = () => {
  const [formState, setFormState] = useState(initialState);
  const todos = useAppSelector(selectTodos);
  const dispatch = useAppDispatch();

  useEffect(() => {
    dispatch(fetchAsyncTodos());
  }, [dispatch]);

  const setInput = (key: string, value: string) => {
    setFormState({ ...formState, [key]: value });
  };

  return (
    <div style={styles.container}>
      <h2>Amplify Todos</h2>
      <input
        onChange={(event) => setInput('name', event.target.value)}
        style={styles.input}
        value={formState.name}
        placeholder="Name"
      />
      <input
        onChange={(event) => setInput('description', event.target.value)}
        style={styles.input}
        value={formState.description}
        placeholder="Description"
      />
      <button
        style={styles.button}
        onClick={() => {
          dispatch(createAsyncTodo(formState));
          setFormState(initialState);
        }}
      >
        Create Todo
      </button>
      {todos.map((todo, index) => (
        <div key={todo.id ? todo.id : index} style={styles.todo}>
          <p style={styles.todoName}>{todo.name}</p>
          <p style={styles.todoDescription}>{todo.description}</p>
        </div>
      ))}
    </div>
  );
};

const styles: {
  [key: string]: React.CSSProperties;
} = {
  container: {
    width: 400,
    margin: '0 auto',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center',
    padding: 20,
  },
  todo: { marginBottom: 15 },
  input: { border: 'none', backgroundColor: '#ddd', marginBottom: 10, padding: 8, fontSize: 18 },
  todoName: { fontSize: 20, fontWeight: 'bold' },
  todoDescription: { marginBottom: 0 },
  button: {
    backgroundColor: 'black',
    color: 'white',
    outline: 'none',
    fontSize: 18,
    padding: '12px 0px',
  },
};

export default withAuthenticator(App);
```

## 苦労した点

チュートリアルはJavaScriptで記載されているため、TypeScriptに変更するには各所で型をしていく必要があります。まずはチュートリアル通りJavaScriptで写経して、VS Codeでエラーの箇所を少しずつ修正していきました。とくにGraphQLの型指定がなかなかうまくはまらず、いろいろなページを調べながら解決、もしくは`as`多様で乗り切っています。。。

また、チュートリアルにある`const styles`の部分についてもエラーを回避するためにインデックスシグネチャを利用していますが、型安全ではないということのようなので別の記載にしたほうがよいのかもしれません。styleくらいなら大丈夫？？？

## さいごに

AWS Amplifyのチュートリアルを体験するだけでも、Amplifyの便利さを体験できると思います。ぜひ試してみてください。

## 参考サイト

https://zenn.dev/ynakamura/articles/210460b470f7b973e5cc

https://qiita.com/otanu/items/2c522a652e5843a5e2c5

https://qiita.com/aakasaka/items/0b081c90b1b99b82143c