Amplify(Next.js, Typescript) 2_複数テーブルのリレーション編

Amplify ドキュメント

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

https://docs.amplify.aws/cli/

■15. 投稿ページの下準備

uuidというのを利用するので、インストール

yarn add uuid

pages/create-post.jsを作成。

まずは、第一段階目のコーディング。

import { withAuthenticator } from "@aws-amplify/ui-react";
import { useState, useRef, React } from "react";
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { v4 as uuid } from "uuid";

const initialState = {title: "", content: ""}

function CreatePost(){
  const [post, setPost] = useState(initialState)
  const { title, content } = post
  const router = useRouter

  function onChange(e){
    setPost(() => ({
      ...post, [e.target.name]: e.target.value
    }))
  }

    async function createNewPost(){
      if(!title || !content) return;
      const id = uuid();
    }
}

export default CreatePost;

ここからAPIを利用して、GraphQLのMutationでCreatePostができるようにしていく。

import { withAuthenticator } from "@aws-amplify/ui-react";
import { useState, useRef, React } from "react";
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { v4 as uuid } from "uuid";
import { createPost } from "../src/graphql/mutations"

const initialState = {title: "", content: ""}

function CreatePost(){
  const [post, setPost] = useState(initialState)
  const { title, content } = post
  const router = useRouter

  function onChange(e){
    setPost(() => ({
      ...post, [e.target.name]: e.target.value
    }))
  }

  async function createNewPost(){
    if(!title || !content) return;
    const id = uuid();
    post.id = id

    await API.graphql({
      query: createPost,
      variables: {input: post},
      authMode: "AMAZON_COGNITO_USER_POOLS"
    })
    // router.push(`/posst/${id}`)
  }

  return(
    <div>
      <h1>Create new Post</h1>
    </div>
  )
}
export default CreatePost;

ここまではまだ準備段階。

router.pushもコメントアウトしておく。

まだh1が表示されるだけである。

■16. 投稿ページのフォーム作成

15の続き。

wigetフォームを簡単に作成できるモジュールをインストール。

SimpleMDEはコードも書ける!

npm install react-simplemde-editor react-markdown

create-post.jsへインポートする。

(中略)
import SimpleMDE from "react-simplemde-editor"
import "easymde/dist/easymde.min.css"

タイトルの入力欄と、ウィジェットフォームを表示させるようにする。

(中略)
 return(
    <div>
      <h1 className="text-3xl font-semibold tracking-wide mt-6">Create new Post</h1>
      <input
        onChange={onChange}
        name="title"
        placeholder="Title"
        value={post.title}
        className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
      />
    <SimpleMDE
      value={post.content}
      onChange={(value) => setPost({...post, content: value})}
    />
    </div>
  )
(中略)

投稿できるようするために、ウィジェットの下に「投稿を作成する」ボタンを追加する。

    <button type="button"
      className="mb-4 bg-blue-600 text-white font-semibold px-8 py-2 rounded-lg"
      onClick={createNewPost}
    >
      Create Post
    </button>

非同期で実行されるcreateNewPost関数は、titleまたはcontentがfalsyの場合、何も中身のないものがreturnされる。

中身があれば、uuidで、世界で一つだけのidを取得し、postのidに設定される。

そして、待っていた API,graphqlが稼働する。

実行されるクエリはcreatePost、 variableはinputで投稿(post)されたもの。投稿にはコンテンツタイトルを付ける必要がある。

そして、これにより、authModeがAmazonのコグニートのユーザープールにあるものとして認識されるようになる。

したがって、ログインした人だけが、投稿を作成する権限がある。

しかし、このままでは駄目。

CreatePostコンポーネントは、 withAuthenticator でラップしなければ、機能しない。

export default withAuthenticator(CreatePost);

しかし、これでも、まだ駄目。

更新すると、参照エラーで「ナビゲーターが定義されていない」と出る。

これは、SSR、つまりサーバー側のレンダリングと関係がある。

それは

import SimpleMDE from "react-simplemde-editor"

に原因がある。

実は、SSRレンダリングと互換性が薄い。

そのため、サーバー側のレンダリングをするためには、少し手を加える必要がある。

ここで登場するのがdynamicインポート。

JavaScriptモジュールを動的にインポートして操作できます。また、SSRでも機能します。」

とのこと。

import dynamic from "next/dynamic";
const SimpleMDE = dynamic(() => import("react-simplemde-editor"), {ssr: false})

この文法通り、SSRのtoggleを渡す必要がある。

ここまでの全体感。

import { withAuthenticator } from "@aws-amplify/ui-react";
import { useState, useRef, React } from "react";
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { v4 as uuid } from "uuid";
import { createPost } from "../src/graphql/mutations"
import dynamic from "next/dynamic";
const SimpleMDE = dynamic(() => import("react-simplemde-editor"), {ssr: false})
// import SimpleMDE from "react-simplemde-editor"
import "easymde/dist/easymde.min.css"
const initialState = {title: "", content: ""}
function CreatePost(){
  const [post, setPost] = useState(initialState)
  const { title, content } = post
  const router = useRouter
  function onChange(e){
    setPost(() => ({
      ...post, [e.target.name]: e.target.value
    }))
  }
  async function createNewPost(){
    if(!title || !content) return;
    const id = uuid();
    post.id = id

    await API.graphql({
      query: createPost,
      variables: {input: post},
      authMode: "AMAZON_COGNITO_USER_POOLS"
    })
    // router.push(`/post/${id}`)
  }
  return(
    <div>
      <h1 className="text-3xl font-semibold tracking-wide mt-6">Create new Post</h1>
      <input
        onChange={onChange}
        name="title"
        placeholder="Title"
        value={post.title}
        className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
      />
    <SimpleMDE
      value={post.content}
      onChange={(value) => setPost({...post, content: value})}
    />
    <button type="button"
      className="mb-4 bg-blue-600 text-white font-semibold px-8 py-2 rounded-lg"
      onClick={createNewPost}
    >
      Create Post
    </button>
    </div>
  )
}
export default withAuthenticator(CreatePost);

■17. 動的IDと新しく作成された投稿の表示

まずは、pagesディレクトリに新しく posts/[id].js を新規作成。

ここでの考え方は、作成投稿に戻って、先に進むということです。

import { API } from 'aws-amplify'
import { useRouter } from 'next/router'
import ReactMarkDown from 'react-markdown'
import '../../configureAmplify'
import { listPosts, getPost } from '../../src/graphql/queries'
export default function Post({post}){
  const router = useRouter()
  if(router.isFallback){
    return <div>Loading...</div>
  }
  return(
    <div>
      <h1 className='text-5xl mt-4 font-semibold tracing-wide'>
        {post.title}
      </h1>
    </div>
  )
}

これで、create-post.jsでコメントアウトしていた、router.pushを解除することができる。

import { withAuthenticator } from "@aws-amplify/ui-react";
import { useState, useRef, React } from "react";
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { v4 as uuid } from "uuid";
import { createPost } from "../src/graphql/mutations"
import dynamic from "next/dynamic";

const SimpleMDE = dynamic(() => import("react-simplemde-editor"), {ssr: false})
// import SimpleMDE from "react-simplemde-editor"
import "easymde/dist/easymde.min.css"
const initialState = {title: "", content: ""}
function CreatePost(){
  const [post, setPost] = useState(initialState)
  const { title, content } = post
  const router = useRouter
  function onChange(e){
    setPost(() => ({
      ...post, [e.target.name]: e.target.value
    }))
  }
  async function createNewPost(){
    if(!title || !content) return;
    const id = uuid();
    post.id = id

    await API.graphql({
      query: createPost,
      variables: {input: post},
      authMode: "AMAZON_COGNITO_USER_POOLS"
    })
    router.push(`/post/${id}`)
  }
  return(
    <div>
      <h1 className="text-3xl font-semibold tracking-wide mt-6">Create new Post</h1>
      <input
        onChange={onChange}
        name="title"
        placeholder="Title"
        value={post.title}
        className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
      />
    <SimpleMDE
      value={post.content}
      onChange={(value) => setPost({...post, content: value})}
    />
    <button type="button"
      className="mb-4 bg-blue-600 text-white font-semibold px-8 py-2 rounded-lg"
      onClick={createNewPost}
    >
      Create Post
    </button>
    </div>
  )
}
export default withAuthenticator(CreatePost);

しかし、このままでは、ルーターは認識するが、投稿を表示することはできない。

何か足りないか?

「Decrypt」、投稿自体を復号化し、自分自身をレンダリングする、という方法がある。

それは、getStaticPathsを使うことになる。

これによって、表示したい場所へのパスが得られる。

[id].js

import { API } from 'aws-amplify'
import { useRouter } from 'next/router'
import ReactMarkDown from 'react-markdown'
import '../../configureAmplify'
import { listPosts, getPost } from '../../src/graphql/queries'
export default function Post({post}){
  const router = useRouter()
  if(router.isFallback){
    return <div>Loading...</div>
  }
  return(
    <div>
      <h1 className='text-5xl mt-4 font-semibold tracing-wide'>
        {post.title}
      </h1>
    </div>
  )
}

export async function getStaticPaths(){
  const postData = await API.graphql({
    query: listPosts
  })
  const paths = postData.data.listPosts.items.map(post => {
    params: { id:post.id }
  })
  return{
    paths,
    falback: true
  }
}

次に、投稿データを渡すことができるようにする別の関数を作成する必要がある。

getStaticPropsを使う。

全体感としては、以下になる。

import { API } from 'aws-amplify'
import { useRouter } from 'next/router'
import ReactMarkDown from 'react-markdown'
import '../../configureAmplify'
import { listPosts, getPost } from '../../src/graphql/queries'
export default function Post({post}){
  const router = useRouter()
  if(router.isFallback){
    return <div>Loading...</div>
  }
  return(
    <div>
      <h1 className='text-5xl mt-4 font-semibold tracing-wide'>
        {post.title}
      </h1>
    </div>
  )
}
export async function getStaticPaths(){
  const postData = await API.graphql({
    query: listPosts
  })
  const paths = postData.data.listPosts.items.map((post) => ({
    params: { id:post.id }
  }))
  return{
    paths,
    fallback: true
  }
}
export async function getStaticProps({params}){
  const { id } = params
  const postData = await API.graphql({
    query: getPost,
    variables: {id},
  })
  return {
    props :{
      post: postData.data.getPost
    }
  }
}

■18. インデックスルートにマークアップとスタイリングを追加する

まずは、getStaticPropsにプロパティをもう一つ追加する。

export async function getStaticProps({params}){
  const { id } = params
  const postData = await API.graphql({
    query: getPost,
    variables: {id},
  })
  return {
    props :{
      post: postData.data.getPost
    },
    revalidate: 1
  }
}

これで、[id].jsの全体感としては、以上になる。

import { API } from 'aws-amplify'

import { useRouter } from 'next/router'

import ReactMarkDown from 'react-markdown'

import '../../configureAmplify'

import { listPosts, getPost } from '../../src/graphql/queries'

export default function Post({post}){

  const router = useRouter()

  if(router.isFallback){

    return <div>Loading...</div>

  }

  return(

    <div>

      <h1 className='text-5xl mt-4 font-semibold tracing-wide'> {post.title}</h1>

      <p className='text-sm font-light my-4'>By {post.username}</p>

      <div className="mt-8">

        <p ReactMarkDown="prose">{post.content}</p>

      </div>

    </div>

  )

}

export async function getStaticPaths(){

  const postData = await API.graphql({

    query: listPosts

  })

  const paths = postData.data.listPosts.items.map((post) => ({

    params: { id:post.id }

  }))

  return{

    paths,

    fallback: true

  }

}

export async function getStaticProps({params}){

  const { id } = params

  const postData = await API.graphql({

    query: getPost,

    variables: {id},

  })

  return {

    props :{

      post: postData.data.getPost

    },

    revalidate: 1

  }

}

index.jsのスタイリングなどの微調整

import { useState, useEffect } from "react"
import { API } from 'aws-amplify'
import { listPosts } from "../src/graphql/queries"
import Link from "next/link"
export default function Home() {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    fetchPosts()
  }, [])
  async function fetchPosts(){
    const postData = await API.graphql({
      query: listPosts
    })
    setPosts(postData.data.listPosts.items)
  }
  return (
    <div>
      <h1 className="text-sky-400 text-3xl font-bold tracking-wide mt-6 mb-2">
        My Post
      </h1>
      {
        posts.map((post, index) => (
          <Link key={index} href={`/post/${post.id}`}>
            <div className="cursor-pointer border-b border-gray-300 mt-8 pb-4">
              <h2 className="text-xl font-semibold" key={index}>{post.title}</h2>
              <p className="text-gray-500 mt-2">Author: {post.username}</p>
            </div>
          </Link>
        ))
      }
    </div>

  )
}

■18. マイ投稿ページの作成