データの受け渡し
json-server
データを作成する
{
"posts":[
{
"id":"1",
"title":"Next.js",
"content":"ヤッホーーーー",
"createdAT":"2024-08-27"
},
{
"id":"2",
"title":"プログラミングわからん",
"content":"これほんとにできるようになるのかな",
"createdAT":"2024-08-26"
}
]
}
json-serverを立ち上げる
複数回立ち上げるのでスクリプトを用意して立ち上げやすいようにする。
またNext.jsで3000版を使用しているので、3001番を指定している。
"scripts": {
~
"json-server":"json-server --watch src/data/posts.json --port 3001"
}
APIの作成と取得
作成されたjson形式のデータをフロント側で取得するために、
APIファイルを作成し、「全記事取得」APIを作成する
import { Article } from "./type";
//非同期処理なのでPromiseを指定し、type.tsのArticleをimport(複数データがあるから配列なのかな?)
export const getAllArticles = async (): Promise<Article[]> => {
//エンドポイントを指定し、ブログのため頻繁にデータ更新が行われる想定なので第二引数でSSRを指定。
const res = await fetch(`http://localhost:3001/posts`, { cache: "no-store" })
//データの文字列化を行う。(なんでjsonでデータをつくったのに、またjsonにする必要があるんだろうか)
const article = await res.json();
return article;
}
TypeScriptでデータのタイプを指定。
export type Article = {
id: string;
title: string;
content: string;
createdAT: string;
}
APIを叩く
blogAPI.tsで作成したAPIを叩く(呼び出す)作業に入る
import {getAllArticles} from "@/blogAPI"
export default async function Home() {
const articles = await getAllArticles();
console.log(articles);
無事受け取れていることを確認したらpropsで渡す
import Image from "next/image";
import Link from "next/link";
import ArticleList from "./components/ArticleList";
import {getAllArticles} from "@/blogAPI"
export default async function Home() {
const articles = await getAllArticles();
return (
<div className="md:flex">
<section className="w-full md:w-2/3 flex flex-col items-center px-3">
//ここで渡しているが、TypeScriptエラーが出てしまうので、受け取る側で型の指定をしてあげる
<ArticleList articles={articles} /
TypeScriptエラーが出てしまうので、受け取る側で型定義
type ArticleListProps = {
//複数あるので配列で呼び出す
articles: Article[];
}
const ArticleList = ({ articles}:ArticleListProps) => {
return (
<div>
map関数で、データを画面に表示させる
const ArticleList = ({ articles}:ArticleListProps) => {
return (
<div>
{articles.map((article) => (
<article className="flex flex-col shadow my-4" key={article.id}>
<Link href="#" className="hover:opacity-75">
<Image
src="https://source.unsplash.com/collection/1346951/1000x500?sig=3"
alt=""
width={1280}
height={300}
/>
</Link>
<div className="bg-white flex flex-col justify-start p-6">
<Link href="#" className="text-blue-700 font-bold pb-4">technology</Link>
<Link
href="#"
className="text-3xl text-slate-900 font-bold hover:text-gray-700 pb-4">
{article.titele}
</Link>
<p className="text-sm pb-3 text-slate-900">{article.createdAT}</p>
<Link href="#" className="pb-6 text-slate-900">
{article.content}
</Link>
<Link href="#" className="text-pink-800 hover:text-black">続きを読む</Link>
</div>
</article>
))}
</div>
)
}
記事詳細ページの遷移先の設定
href={articles/${article.id}
} と設定することで、http://localhost:3000/articles/1
にページ遷移することができる
const ArticleList = ({ articles}:ArticleListProps) => {
return (
<div>
{articles.map((article) => (
<article className="flex flex-col shadow my-4" key={article.id}>
<Link href={`articles/${article.id}`} className="hover:opacity-75">
エラーページの作成
ルーティング
エラーページはNext.jsのルーティングに組み込まれており、error.tsxファイルを特定の場所に格納することで機能する。
今回はappディレクトリの下に作成する。
このように設置するとルーティングに自動的に組み込んでくれる。pageよりも優先的にerrorを表示してくれる。
<lyaut>
<error>
<page>
import React from 'react'
//関数名は大文字にする!
const Error = () => {
return (
<div>
error
</div>
)
}
export default Error;
//もしres(レスポンス)がokでなければ実行する
if (!res.ok) {
throw new Error("エラーが発生しました");
}
リセットボタン
reset関数が用意されており、クリックして呼び出すと再レンダリングしてくれる。
const Error = ({reset}:{reset:()=>void}) => {
return (
<div>
<h3>エラーが発生しました。</h3>
//クリックしたときにreset関数を呼んであげる。
<button onClick={() => reset()} >もう一度試す</button>
</div>
)
}
ローディングページの作成
クルクルを表示させるために処理完了に1.5秒かけるというコードを追加するのだが、
そもそもawait new Promise((resolve) => setTimeout(resolve, 1500));
を解説。
Promiseには3つの状態(成功、失敗、保留)があり、resolveはPromiseを成功の状態にするもの。
awaitがあることで、Promiseを成功状態にするresolve関数が実行されるのを1.5秒後に待ってくれる環境を作ってくれる。
つまり1.5秒後にPromiseがresolve関数によって成功状態となり、次の処置である「res
をjson形式
に変換する」コードが実行される。
awaitがないと時間に関係なくすぐにresolve関数が実行される。
// 「 1.5秒かけて下記のjson形式を取得する 」 という文
await new Promise((resolve) => setTimeout(resolve, 1500));
const article = await res.json();
return article;
loadingファイルを作成
ファイル構成は以下のようになる
<lyaut>
<error>
<loading>
<page>
loading.tsxの中身の作成
import React from 'react'
const Loading = () => {
return (
<div className='flex items-center justify-center min-h-screen'>
<div
className='w-16 h-16 border-t-4 border-orange-500 rounded-full animate-spin'>
</div>
</div>
)
}
export default Loading
layout.tsxで呼び出す。
<Suspense></Suspense>
で囲まれたコンポーネントの読み込みが完了するまで、fallbackで設定したコンポーネントを代わりに表示させることができる。
return (
<html lang="ja">
<body className="container mx-auto bg-slate-700 text-slate-50">
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow">
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</main>
<Footer />
</div>
</body>
</html>
);
記事詳細ページに遷移させる
下記のようにarticles/[id]
というページに遷移させて、そのidに合うページを表示させる。
<Link href={`articles/${article.id}`}>
{article.title}
</Link>
paramsを使用するとURLからidを取得することができる。
流れは
①id:7の記事をクリックする
↓
②http://localhost:3000/articles/7
に遷移する
↓
③params
でURL/7
からidを取得する
const Article = ({ params }: { params: {id: string} }) => {
↓
④id:7の情報をゲットしたので、APIを叩く。
API作成
//引数でidを取得(引数でidを指定しないと機能しないのかな?)
export const getDetailArticles = async (id: string): Promise<Article[]> => {
//エンドポイントで${id}を指定。json-serverで実際に下記URLで詳細を確認することができる
const res = await fetch(`http://localhost:3001/posts/${id}`, { next: { revalidate: 60 }, })//ISR
//指定したエンドポイントが存在しない場合はnotFuondを呼び出す
if (res.status === 404) {
//notFound関数が元々用意されており、notFoundページを用意した時にそこにページ遷移する。
notFound();
}
if (!res.ok) {
throw new Error("エラーが発生しました");
}
await new Promise((resolve) => setTimeout(resolve, 1500));
const article = await res.json();
return article;
}
APIを叩く
import Image from 'next/image';
import React from 'react'
import {getDetailArticles} from "@/blogAPI"
const Article = async ({ params }: { params: { id: string } }) => {
//propsのparamsからidが回ってくる
const detailArticle = await getDetailArticles(params.id);
return (
<div className='max-w-3xl mx-auto p-5'>
<Image
src="https://source.unsplash.com/collection/1346951/1000x500?sig=3"
alt=""
width={1280}
height={300}
/>
<h1 className='text-4xl text-center mb-10 mt-10'>{detailArticle.title}</h1>
<div className='text-lg leading-relaxed text-justify'>
<p>{detailArticle.content}</p>
</div>
</div>
)
}
NotFoundページの作成
Next.jsではerrorと同じようにnot-found.js
を用意してくれている。
articleディレクトに設置したので、articleディレクトリ配下でのみ適用される。
記事投稿機能の作成
APIの作成
export const creteArticles = async (id: string, title: string, content: string): Promise<Article> => {
//現在の日時を取得
const currentDatetime = new Date().toISOString();
const res = await fetch(`http://localhost:3001/posts`, {
method: "POST",
//headerでデータのファイル形式のを指定。
//指定しないと受け取り側は推測してtypeを決めるらしいので、bodyで送信するjsonを
//認識してくれない可能性あり。json-serverはjsonで管理しているので、今回はjsonを指定。
headers: {
"Content-Type":"application/json",
},
//実際に何をリクエストするかを記述。
//JSON.stringify({id,title,content})によって、実際にデータがjson形式になる。
//もしJSON.stringify()を使用しないと、オブジェクトを文字列に変換した
//`[object Object]` という文字列が送信されてしまう。
body:JSON.stringify({ id,title,content,createdAT:currentDatetime }),
});
if (!res.ok) {
throw new Error("エラーが発生しました");
}
await new Promise((resolve) => setTimeout(resolve, 1500));
const newArticle = await res.json();
return newArticle;
}
creteArticlesを呼び出してAPIを叩く
作成したいのは、クリックしたらformに入力したい内容をサーバーに保存すること。
そのために入力した「URL、title、content」をcreteArticles()に引数で渡してあげる。
const CreateBlogPage = () => {
const [id, setId] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [content, setContent] = useState<string>("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(id,title,content);
//引数に実際に入力した値を埋め込むことで、createArticle関数に情報が渡る
await creteArticles(id,title,content);
}
return (
<div className='min-h-screen'>
<h2 className='text-2xl font-bold mb-4 py-8 px-4 md:px-12'>投稿ページ</h2>
<form
className='bg-slate-200 p-6 rounded shadow-lg'
onSubmit={handleSubmit}
>
<div>
<label className='text-gray-700 text-sm font-bold md-2'>URL</label>
<input
type="text"
className='shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none'
onChange={(e)=>setId(e.target.value)}
/>
</div>
<div className='mb-4'>
<label className='text-gray-700 text-sm font-bold md-2'>タイトル</label>
<input
type="text"
className='shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none'
onChange={(e)=>setTitle(e.target.value)}
/>
</div>
<div className='mb-4'>
<label className='text-gray-700 text-sm font-bold md-2'>本文</label>
<textarea
className='shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none'
onChange={(e)=>setContent(e.target.value)}
/>
</div>
<button type="submit" className='py-2 px-4 border rounded-md bg-orange-300'>投稿</button>
</form>
</div>
)
}
流れをまとめるとformに入力した内容が、handleSubmitによって呼ばれたcreteArticles関数によってblogAPIのcreteArticles関数に渡される。
fetchによってサーバーと接続されているが、今回はPOSTなのでフロントサイドからサーバーサイドにデーターをjson形式で送信している。
返ってきたレスポンスをjson形式でnewArticle変数に代入して、newArticleを返している。
投稿ボタンを押したらトップページにリダイレクトさせる
(※気になる記事)
https://zenn.dev/yoshiishunichi/articles/ed67d3cf1b9b41
Linkタグを使えない場所や画面遷移しながらプログラムの実行をするときはuseRouter(next/navigation)
を使用する。
refreshしないと作った記事が遷移後のページに反映されないので注意!
const CreateBlogPage = () => {
const router = useRouter();
//〜
//creteArticles()関数を呼び出してから画面遷移。
router.push("/");
//refreshしないと投稿した記事がトップページに反映されない!
router.refresh();
}
重複送信を防ぐ
disabled={loading}
を追加し、loadnig
がtrue
の時はbutton
が押せないように設定。
//追加
const [loading, setLoading] = useState<boolean>(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
//追加。投稿ボタンを押したらloadingをtrueに変更する
setLoading(true);
await creteArticles(id, title, content);
//追加。データ送信が完了したらloadingをfalseに戻してあげる
setLoading(false);
router.push("/");
router.refresh();
}
//〜
<button
type="submit"
className={`py-2 px-4 border rounded-md ${loading ? "bg-orange-300 cursor-not-allowed" : "bg-orange-400 hover:bg-orange-500"}`}
//重複投稿を防ぐためにloadingがtrueの時は何回もボタンが押せないようにする。
disabled={loading}
>
投稿
</button>
投稿の削除
削除ボタンを設置したいが、onClick
は'use client'
出ないとエラーが出てしまう。
button
だけ'use client'
にしたいので、別コンポーネントを用意。
return (
<div className='max-w-3xl mx-auto p-5'>
<Image
src="https://source.unsplash.com/collection/1346951/1000x500?sig=3"
alt=""
width={1280}
height={300}
/>
<h1 className='text-4xl text-center mb-10 mt-10'>{detailArticle.title}</h1>
<div className='text-lg leading-relaxed text-justify'>
<p>{detailArticle.content}</p>
</div>
<div className='text-right mt-3'>
{/* ボタンだけuse clientにしたいのでコンポーネントで分ける */}
<DeleteButton id={detailArticle.id} />
</div>
</div>
)
'use client'
import React from 'react'
const DeleteButton = () => {
return (
<div>
</div>
)
}
export default DeleteButton
文字数制限
{/* substringは「⚪︎文字〜⚪︎文字を切り取る」という関数。*/}
{/*length > 70の時 ? trueなら : falseなら */}
{article.content.length > 70
? article.content.substring(0,70) + "..."
: article.content
}
supabaseとの連携
supabaseのセットアップ
New Project
で新しくプロジェクトを作成する。
作成後時間が経つとwelcome画面が表示されるので、
その画面に表示されるProject URL
とAPI Key
を用いて、localで作成する環境変数の準備を行う。
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
公式ドキュメントを確認しながらセットアップを行う
https://supabase.com/docs/reference/javascript/installing
supabaseのインストール
npm install @supabase/supabase-js
Initializing(初期化)
import { createClient } from "@supabase/supabase-js";
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
persistSession:false,
}
})
データベースの作成
①New table
でテーブルを作成する。
②試しにInsert
をおして一つ手動で追加してみる
supabase用のAPI作成
※なぜpagesなのかは謎
import { supabase } from "@/utils/supabaseClient";
import { NextApiRequest, NextApiResponse } from "next";
//全記事取得API
export default async function handler(
rep: NextApiRequest,
res: NextApiResponse
) {
const { data, error } = await supabase.from("posts").select("*");
if (error) {
return res.status(500).json({ error: error.message });
}
return res.status(200).json(data);
}
APIを叩く
.env.localにエンドポイントを指定
NEXT_PUBLIC_API_URL=http://localhost:3000
export default async function Home() {
const API_API = process.env.NEXT_PUBLIC_API_URL;
//pages/apiを指定
const res = await fetch(`${API_API}/api`, { cache: "no-store" });
const articles = await res.json();
なぜfetchで`locallhost::3000`を指定しないといけないのか
Next.jsのAPIルートは、サーバーサイドのAPIエンドポイントを簡単に作成できる機能です。
pages/api ディレクトリ内にJavaScriptファイルを作成するだけで、
自動的にAPIエンドポイントとして機能します。
とのこと
つまり、下記のように役割を明確に分けることで安全・かつ管理しやすくなる。
supabaseとフロントエンド(page.tsx)は直接やりとりしない!
------------------------
supabase ←→ バックエンドAPI(pages/api)${API_API}/api
バックエンドAPI(pages/api)${API_API}/api ←→ フロントエンド(page.tsx)
------------------------
詳しくは「API Routerとは」で調べてみる
あとはjson-serverで作成したコードでOK!