0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsとjson-serverとsupabase

Posted at

データの受け渡し

json-server

データを作成する

src / data / posts.json
{
  "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番を指定している。

package.json
"scripts": {
    ~
    "json-server":"json-server --watch src/data/posts.json --port 3001"
  }

APIの作成と取得

作成されたjson形式のデータをフロント側で取得するために、
APIファイルを作成し、「全記事取得」APIを作成する

src/blogAPI.ts
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でデータのタイプを指定。

src/type.ts
export type Article = {
  id: string;
  title: string;
  content: string;
  createdAT: string;
}

APIを叩く

blogAPI.tsで作成したAPIを叩く(呼び出す)作業に入る

page.tsx
import {getAllArticles} from "@/blogAPI"

export default async function Home() {
  const articles = await getAllArticles();
  console.log(articles);

無事受け取れていることを確認したらpropsで渡す

page.tsx
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エラーが出てしまうので、受け取る側で型定義

ArticleList.tsx
type ArticleListProps = {
  //複数あるので配列で呼び出す
  articles: Article[];
}

const ArticleList = ({ articles}:ArticleListProps) => {
  return (
    <div>

map関数で、データを画面に表示させる

ArticleList.tsx
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
にページ遷移することができる

ArticleList.tsx
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>

ファイル構成

app/error.tsx
import React from 'react'
//関数名は大文字にする!
const Error = () => {
  return (
    <div>
      error
    </div>
  )
}

export default Error;
blogAPI.tsx
//もしres(レスポンス)がokでなければ実行する
if (!res.ok) {
    throw new Error("エラーが発生しました");
}

リセットボタン

reset関数が用意されており、クリックして呼び出すと再レンダリングしてくれる。

app/error.tsx
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関数によって成功状態となり、次の処置である「resjson形式に変換する」コードが実行される。

awaitがないと時間に関係なくすぐにresolve関数が実行される。

blogAPI.tsx
 // 「 1.5秒かけて下記のjson形式を取得する 」 という文
  await new Promise((resolve) => setTimeout(resolve, 1500));

  const article = await res.json();
  return article;

loadingファイルを作成

loading.tsxを作成する。
ファイル構成

ファイル構成は以下のようになる

<lyaut>
    <error>
        <loading>
            <page>

loading.tsxの中身の作成

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で設定したコンポーネントを代わりに表示させることができる。

layout.tsx
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を取得する

page.tsx
const Article = ({ params }: { params: {id: string} }) => {


④id:7の情報をゲットしたので、APIを叩く。

API作成

blogAPI.tsx
//引数で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を叩く

page.tsx
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の作成

blogAPI.tsx
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()に引数で渡してあげる。

page.tsx
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しないと作った記事が遷移後のページに反映されないので注意!

page.tsx
const CreateBlogPage = () => {
  const router = useRouter();
  
    //〜
    
    //creteArticles()関数を呼び出してから画面遷移。
      router.push("/");
      //refreshしないと投稿した記事がトップページに反映されない!
      router.refresh();
    }

重複送信を防ぐ

disabled={loading}を追加し、loadnigtrueの時はbuttonが押せないように設定。

page.tsx
//追加
  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'にしたいので、別コンポーネントを用意。

page.tsx
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>
  )
DeleteButton.tsx
'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 URLAPI Keyを用いて、localで作成する環境変数の準備を行う。

.env.local
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

公式ドキュメントを確認しながらセットアップを行う
https://supabase.com/docs/reference/javascript/installing

supabaseのインストール

npm install @supabase/supabase-js

Initializing(初期化)

src/utils/supabaseClient.ts
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なのかは謎

src/pages/api/index.ts
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にエンドポイントを指定

.env.local
NEXT_PUBLIC_API_URL=http://localhost:3000
page.tsx
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!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?