Amplify(Next.js, Typescript) 2_複数テーブルのリレーション編
Amplify ドキュメント
https://docs.amplify.aws/start/q/integration/next/
■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"
に原因がある。
そのため、サーバー側のレンダリングをするためには、少し手を加える必要がある。
ここで登場するのが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. マイ投稿ページの作成