Chapter 04

Todo一覧を表示する

shitakemura
shitakemura
2022.02.12に更新

はじめに

本チャプターではフロントエンドアプリから graphQL API を呼び出し、Todo 一覧画面の表示を行います。バックエンドと同様にGraphQLクエリを作成 -> TypeScript の型定義を自動生成 -> 実装の流れでおこないます。

GraphQL クエリを定義する

Todo の一覧取得で呼びだすクエリGetTodosの定義ファイルを作成します。

./frontend/graphql/GetTodos.graphql
query GetTodos {
  getTodos {
    id
    title
    completed
    createdAt
  }
}

クエリ定義から TypeScript の型定義ファイルを自動生成する

GraphQL Code Generatorを使い、必要な型定義ファイルを自動生成します。

まず、必要なパッケージをインストールします。

./frontend
$ npm i @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @graphql-codegen/named-operations-object

続いて、GraphQL Code Generator の設定ファイルcodegen.ymlを作成します。

バックエンドで定義したスキーマ定義./infra-backend/graphql/schema.graphqlと、フロントエンドで定義したクエリ定義./graphql/*.graphqlを使って型情報を生成し、./graphql/generated/generated-types.tsに出力します。

./frontend/codegen.yml
overwrite: true
schema: "../infra-backend/graphql/schema.graphql"
documents: "./graphql/*.graphql"
generates:
  ./graphql/generated/generated-types.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
      - named-operations-object

npm コマンドから型情報の生成が行えるようpackage.jsonを更新します。

./frontend/package.json
  "scripts": {
    ...
+    "codegen": "graphql-codegen"
  }

npm コマンドを実行します。

./frontend
$ npm run codegen

generated-types.tsに型定義が生成されているのを確認します。

画面 UI の作成

Chakra UI を導入する

画面 UI の作成にはChakra UIを使用しました。
必要なパッケージをインストールします。

./frontend
$ npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^5 @chakra-ui/icons

セットアップを行います。

./frontend/pages/_app.tsx
+ import { ChakraProvider } from "@chakra-ui/react";

function MyApp({ Component, pageProps }: AppProps) {
  return (
+    <ChakraProvider>
      <Component {...pageProps} />
+    </ChakraProvider>
  );
}

続いて、以下の通りコンポーネントを作成、更新します。

./frontend/components/Todo

./frontend/components/Todo/TodoItem.tsx
import { useState } from "react";
import { HStack, Text } from "@chakra-ui/react";
import { Todo } from "../../graphql/generated/generated-types";

type TodoItemProps = {
  todo: Todo;
};

export const TodoItem = ({ todo }: TodoItemProps) => {
  const [todoItem] = useState<Todo>(todo);

  return (
    <HStack
      borderColor='blue.300'
      borderWidth={1}
      p={8}
      w='full'
      height='16'
      justify='space-between'
      spacing={8}>
      <Text
        textDecoration={todoItem.completed ? "line-through" : undefined}
        color={todoItem.completed ? "gray.500" : "black"}>
        {todoItem.title}
      </Text>
    </HStack>
  );
};
./frontend/components/Todo/TodoList.tsx
import { VStack } from "@chakra-ui/react";
import { Todo } from "../../graphql/generated/generated-types";
import { TodoItem } from "./TodoItem";

type TodoListProps = {
  todos: Todo[],
};

export const TodoList = ({ todos }: TodoListProps) => {
  return (
    <VStack w='full' paddingX={8}>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </VStack>
  );
};
./frontend/components/Todo/TodoScreen.tsx
import { VStack } from "@chakra-ui/react";
import { Todo } from "../../graphql/generated/generated-types";
import { TodoList } from "./TodoList";

export const TodoScreen = () => {
  const todos: Todo[] = [
    {
      id: "todo0123",
      title: "test todo",
      completed: true,
      createdAt: 1644558686117,
    },
    {
      id: "todo7637",
      title: "test todo 2",
      completed: false,
      createdAt: 1644558686117,
    },
  ];

  return (
    <VStack w='full' spacing={10} paddingX={48} paddingY={16}>
      <TodoList todos={todos} />
    </VStack>
  );
};
./frontend/components/Todo/index.ts
export * from "./TodoScreen";

./frontend/components/Header

./frontend/components/Header/Header.tsx
import { Flex, Text } from "@chakra-ui/react";

type HeaderContainerProps = {
  children: React.ReactNode,
};

const HeaderContainer = ({ children }: HeaderContainerProps) => {
  return (
    <Flex
      align='center'
      justify='space-between'
      wrap='wrap'
      w='100%'
      p={8}
      bg='blue.400'
      color='white'>
      {children}
    </Flex>
  );
};

export const Header = () => {
  return (
    <HeaderContainer>
      <Text fontSize={24} fontWeight='bold'>
        Todo App
      </Text>
    </HeaderContainer>
  );
};
./frontend/components/Header/index.ts
export * from "./Header";

./frontend/components/Home

./frontend/components/Home/Home.tsx
import { VStack } from "@chakra-ui/react";
import { Header } from "../Header";
import { TodoScreen } from "../Todo";

export const Home = () => {
  return (
    <VStack>
      <Header />
      <TodoScreen />
    </VStack>
  );
};
./frontend/components/Home/index.ts
export * from "./Home";

./frontend/pages

./frontend/pages/index.tsx
+ import { Home } from "../components/Home";

- const Home: NextPage = () => {
+ const HomePage: NextPage = () => {
  return (
    <div>
      ...
      <main>
+        <Home />
      </main>
    </div>
  );
};

- export default Home;
+ export default HomePage;

npm run devを実行して確認します。

Apollo Client を導入する

GraphQL API の操作のためApollo Clientを導入します。

必要なパッケージをインストールします。

./frontend
$ npm install @apollo/client graphql

初期化の処理を追加します。

./frontend/components/Home/Home.tsx
+ import {
+   ApolloClient,
+   InMemoryCache,
+   ApolloProvider,
+   HttpLink,
+ } from "@apollo/client";

+ const client = new ApolloClient({
+  link: new HttpLink({
+     uri: process.env.NEXT_PUBLIC_APPSYNC_API_URL,
+     headers: {
+       "X-Api-Key": process.env.NEXT_PUBLIC_APPSYNC_API_KEY,
+     },
+   }),
+   cache: new InMemoryCache(),
+ });

export const Home = () => {
  return (
    <VStack>
+       <ApolloProvider client={client}>
        <Header />
        <TodoScreen />
+      </ApolloProvider>
    </VStack>
  );
};

AWS 管理画面のAWS AppSync > Todo-graphql-api > Settingsより値を確認し、.envファイルを作成します。

 ./frontend/.env.local
NEXT_PUBLIC_APPSYNC_API_URL="YOUR-APPSYNC-API-URL"
NEXT_PUBLIC_APPSYNC_API_KEY="YOUR-APPSYNC-API-KEY"

GraphQL API を実行して、Todo 一覧を取得、画面に表示する

クエリgetTodosでデータを取得し、一覧画面に表示します。
クエリ定義をもとにGraphQL Code Generatorで作成したuseGetTodoQuery()を使用して Todo 一覧を取得します。

./frontend/components/Todo/TodoScreen.tsx
- import { Todo } from "../../graphql/generated/generated-types";
+ import { useGetTodosQuery } from "../../graphql/generated/generated-types";

export const TodoScreen = () => {
-    const todos: Todo[] = [
-    {
-      id: "todo0123",
-      title: "test todo",
-      completed: true,
-      createdAt: 1644558686117,
-    },
-    {
-      id: "todo7637",
-      title: "test todo 2",
-      completed: false,
-      createdAt: 1644558686117,
-    },
-  ];

+  const { data } = useGetTodosQuery();

  return (
    <VStack w='full' spacing={10} paddingX={48} paddingY={16}>
-      <TodoList todos={todos} />
+      <TodoList todos={data?.getTodos ?? []} />
    </VStack>
  );
};

npm run devを実行して DynamoDB に格納されている Todo が表示されるか確認します。

以上で Todo 一覧の表示は完了です。

参考情報