spring1018 / react-ui

0 stars 0 forks source link

実践 Next.js #85

Open spring1018 opened 6 months ago

spring1018 commented 6 months ago
spring1018 commented 6 months ago

第1章:Next.jsの基礎

プロジェクトセットアップ

ルーティング

SPAならではのナビゲーション

ネスト可能なレイアウト

spring1018 commented 6 months ago

第2章:Server Componentとレンダリング

Server Component と Client Component (RSC/CC)

SC

CC

SC・CCの使い分け

// training/packages/training-web-2/src/app/photos/[photoId]/LikeButton.tsx
 "use client"; // ★: "use client" ディレクティブを追加する

 export function LikeButton({ photoId }: { photoId: string }) {
   // ★: onClick イベントハンドラーを追加したい
   return (
     <button
       onClick={() => {
         console.log(`photoId ${photoId} が「いいね」されました`);
       }}
     >
       いいね
     </button>
   );
 }

SCのデータ取得

// Server Component の実装例
 export default async function ServerComponent() {
   const res = await fetch("https://example.com/posts");
   const posts = await res.json();
   return <div>{/* posts データを使用した表示 */}</div>;
 }
async function getPhotos() {
  const data: { photos: Photo[] } = await fetch(
     "http://localhost:8080/api/photos"
   ).then((res) => res.json());
   return data.photos.map(({ id, title }) => ({ id, title }));
 }

export default async function Page() {
   const photos = await getPhotos(); // <- データを取得
   return (
     <div className={styles.container}>
       <h1>トップ画面</h1>
       <ul>
         {photos.map(({ id, title }) => (
           <li key={id}>
             <Link href={`/photos/${id}`}>{title}</Link>
           </li>
         ))}
       </ul>
     </div>
   );
 }

動的データ取得と静的データ取得

fetch("https://..."); // デフォルトでは静的データ取得となる
fetch("https://...", { cache: "no-store" }); // 動的データ取得

Route のレンダリング

spring1018 commented 6 months ago

第3章:App Routerの規約

3.1 Segment 構成ファイル

通常画面以外の表示の機能も補助する。SCかCCで扱う。 image

<Sugpense fallback={<Spinner />}
  <Comment />
</ Suspense>
// training/training-web-4/src/app/loading.tsx
 export default function Loading() {
   return <>...loading</>;
 }
// my-app/src/app/api/hello/route.ts
 export async function GET(request: Request) {
   return new Response("Hello, Next.js!");
 }

3.2 Segment 構成フォルダ

3.3 Parallel Routes と Intercepting Routes

3.4 Route のメタデータ

spring1018 commented 6 months ago

第4章:Route Handler

4.1 Route Handler の定義

以下は /api/hello のリクエストを処理する。 function 名は HTTP リクエストのメソッドに対応するもの。

// my-app/src/app/api/hello/route.ts
 export async function GET() {
   return new NextResponse("Hello, Next.js!");
 }

4.2 Route Handler のレンダリング

4.3 Router Handler の使用例

以下で、app/api/photos/[photoId]/like/route.ts に届く。 https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/app/api/photos/%5BphotoId%5D/like/route.ts

// training/packages/training-web-4/src/app/(site)/photos/[photoId]/LikeButton.tsx
"use client";

 export function LikeButton({ photoId }: { photoId: string }) {
   return (
     <button
       onClick={() => {
         fetch(`/api/photos/${photoId}/like`, {
           method: "POST",
         });
       }}
     >
       いいね
     </button>
   );
 }
// training/packages/training-web-4/src/app/api/photos/[photoId]/like/route.ts
 export async function POST(
   _: Request,
   { params }: { params: { photoId: string } }
 ) {
   console.log(`photoId ${params.photoId} が「いいね」されました`);
   // TODO: 誰から送られたリクエストかを cookie から特定する処理 
   // TODO: DBサーバーなどに永続化するための処理
   return Response.json({ liked: true });
 }
spring1018 commented 6 months ago

第5章:サンプルアプリの概要

5.1 サンプルアプリのシステム構成

image

5.2 ローカル開発環境の構築

docker desktop のインストールを行って、以下でサーバーが起動することを確認

docker compose up -d

5.3 Prisma の概要

/api/categories の API https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/app/api/categories/route.ts

// applications/packages/sns-api-2/src/app/api/categories/route.ts
 import { prisma } from "@/lib/prisma";

 export async function GET() {
   // ★: Category テーブルのレコードをすべて取得する
   const categories = await prisma.category.findMany({
     include: { _count: { select: { photos: true } } },
   });
   console.log(`GET: /api/categories ${new Date().toISOString()}`);
   return Response.json({
     categories: categories.map(({ _count, ...category }) => ({
       ...category,
       totalPhotoCount: _count.photos,
     })),
   });
 }

5.4 Prisma Studio の概要

視覚的に扱えて便利。自分用の作りたい (企業では有料なので)

5.5 開発環境のストレージサーバー

spring1018 commented 6 months ago

第6章:データ取得とキャッシュ

6.1 共通UIコンポーネント

   7   │   "workspaces": [
   8   │     "packages/sns-api-1",
   9   │     "packages/sns-shared-ui",
  10   │     "packages/sns-web-1",
  11   │     "packages/sns-web-2",
  12   │     "packages/sns-web-3"
  13   │   ],

sns-web-1 側の package.json には dependencies で指定されている

    2   │   "dependencies": {
  23   │     "clsx": "^2.0.0",
  24   │     "date-fns": "^2.30.0",
  25   │     "next": "14.2.2",
  26   │     "react": "^18",
  27   │     "react-dom": "^18",
  28   │     "react-dropzone": "^14.2.3",
  29   │     "sns-shared-ui": "*"
  30   │   },

モノレポにおける Next.js は、別の Next.js プロジェクトのコードを直接 import してトランスパイルできる

// applications/packages/sns-web-1/next.config.mjs
 const nextConfig = {
   // ...
   transpilePackages: ["sns-shared-ui"],
 };

これで、以下のように sns-shared-ui からコンポーネントを import できる

import { HeadGroup } from "sns-shared-ui/src/components/HeadGroup";

6.2 fetch 関数でのデータ取得

fetch 関数をラップした関数を services に整理。 https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/services/getCategory/index.ts

import { handleFailed, handleSucceed, path } from "../";
import type { Category } from "../type";
import type { PaginationProps } from "sns-shared-ui/src/components/Pagination";

export async function getCategory({
  categoryName,
  page = "1",
  take = "10",
}: {
  categoryName: string;
  page?: string;
  take?: string;
}): Promise<{ category: Category; pagination: PaginationProps }> {
  const searchParams = new URLSearchParams({ page, take });
  return fetch(path(`/api/categories/${categoryName}?${searchParams}`), {
    cache: "no-store",
    next: { tags: ["categories"] }, // ★: 抽象的な tag
  })
    .then(handleSucceed)
    .catch(handleFailed);
}

共通関数:

handleSucceed で400以上のレスポンスを reject する。 https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/services/index.ts

export const handleSucceed = async (res: Response) => {
  const data = await res.json();
  if (!res.ok) {
    throw new FetchError(res.statusText, res.status);
  }
  return data;
};

path でホストを共通管理 https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/services/index.ts#L1C1-L3C57

export const host = process.env.API_HOST;
export const path = (path?: string) => `${host}${path}`;

6.3 fetch 関数の Request のメモ化

画面を提供するRouteではSEO観点でページの「title」や「description」が重要になります。3.4節「Routeのメタデータ」で解説したように、これらの「メタデータ」を設定するためには、Next.jsのAPIであるgenerateMetadata関数を使用します。 https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/app/(site)/categories/%5BcategoryName%5D/page.tsx

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/categories/%5B...segments%5D/page.tsx#L58

async function getCategoryFromProps({ params }: Props) {
  const categoryName =
    typeof params.segments[0] === "string" ? params.segments[0] : "1";
  const page =
    typeof params.segments[1] === "string" ? params.segments[1] : "1";
  // ★: /categories/flower/1/2 などの場合は 404 を返す
  if (params.segments.length > 2) {
    notFound();
  }
  const data = await getCategory({
    categoryName,
    page,
    take: `${take}`,
  });
  return { ...data, page: +page };
}

6.4 fetch 関数のキャッシュ

// applications/packages/sns-web-2/src/app/(site)/users/[screenName]/page.tsx
 // 【2】特定したユーザーのIDで、投稿写真一覧を取得
 const { photos } = await getPhotos({
   page: "1",
   take: "15",
   authorId: profile.user.id,
   revalidate: 60 * 60, // ★: 1時間キャッシュする
 });

6.5 Prisma Client でのデータ取得

image image

データ取得方法が分かれている理由は、ユーザー情報を管理しているDB(ps-db-user)が、投稿写真を管理している Web APIサーバーのDB(ps-db-data)と異なるためです。つまり、ユーザー情報の取得や更新はfetch関数ではなく、Prisma Clientを使用するということです。

トップページ: https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/app/(site)/page.tsx

  const users = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      image: true,
      profile: { select: { screenName: true } },
    },
  });

schema: https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/prisma/schema.prisma

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  profileId     String?
  profile       Profile?
  accounts      Account[]
  sessions      Session[]
}

6.6 Prisma Client のリクエストの重複排除

6.7 Prisma Client のキャッシュ

spring1018 commented 6 months ago

第7章:認証機能

7.1 環境変数の設定

Next.js では環境変数の読み込みを標準でサポートしているので dotenv などのライブラリは不要。

$(NODE_ENV) には以下が入る。

これらには読み込みの優先順位がある (.env.local > .env) リポジトリで管理すべきではない環境変数は .env*.local Private な環境変数 (NEXT_PUBLIC_ 接頭辞のないもの) がブラウザ向けバンドルに含まれていることを見つけると、Next.jsは空文字列に置き換える

7.2 OAuth クライアントの作成

Google Cloud でやる、省略

7.3 NextAuth.js の導入

導入手順:

  1. 環境変数 NEXTAUTH_SECRET の設定
  2. Middleware の設置
  3. NextAuthOptions の設定
  4. Route Handler の設置
  5. getServerSession の利用

7.4 ログインユーザーのデータ表示

7.5 閲覧ユーザーに応じた表示分岐

spring1018 commented 6 months ago

第8章:モーダル表示とデータ連携

spring1018 commented 6 months ago

第9章:データ更新とUI

9.1 Server Action の基礎

Server Action の概要

"use server" ディレクティブの宣言

引数の参照 (FormData)

"use client";

import { myAction } from "./actions";

export default function ClientComponent({ id }: { id: string }) {
   return (
     <form action={myAction}>
       <input type="hidden" name="id" value={id} />
       <button type="submit">Add to Cart</button>
     </form>
   );
export default async function myAction(formData: FormData) {
   const id = formData.get("id");
   if (typeof id !== "string") {
     throw new Error("Validation error");
   }
   // ...id を使用した更新処理
 }

引数のバインド (Binding Arguments)

"use client";

 import { myAction } from "./actions";

 export default function ClientComponent({ id }: { id: string }) {
   const action = myAction.bind(null, id);
   return (
     <form action={action}>
       <button type="submit">Add to Cart</button>
     </form>
}
export default async function myAction(id: string, formData: FormData) {
   // ...id を使用した更新処理
}

Progressive Enhancement

9.2 Server Action によるデータ保存

spring1018 commented 6 months ago

第10章:パフォーマンスとキャッシュ

10.1 コンポーネント構造のパフォーマンスへの影響

トップ画面の課題と改善

before: https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-2/src/app/(site)/page.tsx

export default async function Page({ searchParams }: Props) {
  const page = typeof searchParams.page === "string" ? searchParams.page : "1";
  // 【1】最新投稿写真一覧に使用するデータ
  const photosData = await getPhotos({ page });
  // 【2】カテゴリー一覧に使用するデータ
  const categoriesData = await getCategories();
  // 【3】ユーザー一覧に使用するデータ
  const users = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      image: true,
      profile: { select: { screenName: true } },
    },
  });
...

after: https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/page.tsx

export default async function Page({ searchParams }: Props) {
  // 【1】【2】【3】のデータ取得がなくなっている
  return (
    <div className={styles.page}>
      <div className={styles.photos}>
        <TopPhotos searchParams={searchParams} />
      </div>
      <aside className={styles.aside}>
        <TopCategories />
        <TopUsers />
      </aside>
    </div>
  );
}

データ取得が子コンポーネントで行われている https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/_components/TopPhotos/index.tsx#L15

export async function TopPhotos({ searchParams }: Props) {
  const page = typeof searchParams.page === "string" ? searchParams.page : "1";
  // 【1】最新投稿写真一覧に使用するデータ
  const { photos, pagination } = await getPhotos({ page });
  return (
...

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/_components/TopCategories/index.tsx#L10

export async function TopCategories() {
  // 【2】カテゴリー一覧に使用するデータ
  const data = await getCategories();
  return (
...

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/_components/TopUsers/index.tsx#L7

export async function TopUsers() {
  // 【3】ユーザー一覧に使用するデータ
  const users = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      image: true,
      profile: { select: { screenName: true } },
    },
  });
  return (
...
spring1018 commented 6 months ago

付録A:Prisma

A.1 Prisma schema の概要

schema ファイルの書き方

A.2 Prisma Client の概要

シーディング

  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed/index.ts"
  }
spring1018 commented 3 months ago

以下にめっちゃはまった。第二引数で受けるのか・・・

PageファイルなどではPropsからparams参照をしていましたが、Route Handlerではexportする関数の第2引数を参照します。画面のRouteと同様Route Handlerでも、Dynamic Segment値を参照できます。 import { NextRequest, NextResponse }

from "next/server";

 export async function GET(
   request: NextRequest,
   { params }: { params: { photoId: string } }
 ) {
spring1018 commented 1 month ago

実践 Next.js をもとにしたデータ取得実装

page.tsx

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/page.tsx コンポーネントをSCにする。

...
      <div className={styles.photos}>
        <TopPhotos searchParams={searchParams} />
      </div>
...

component

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/app/(site)/_components/TopPhotos/index.tsx#L15 query parameter を取得して関数に渡す。(関数を切り出す) ここでは page だけを対象にする。

import { getPhotos } from "@/services/getPhotos";
...
export async function TopPhotos({ searchParams }: Props) {
  const page = typeof searchParams.page === "string" ? searchParams.page : "1";
  const { photos } = await getPhotos({ page });
...

@/services/getPhotos

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-web-3/src/services/getPhotos/index.ts#L12 キャッシュ処理は一旦省く。

import { handleFailed, handleSucceed, path } from "../";
import type { Photo } from "../type";
import type { PaginationProps } from "sns-shared-ui/src/components/Pagination";

type Props = {
  page?: string;
};

export function getPhotos({
  page = "1",
}: Props): Promise<{ photos: Photo[]; pagination: PaginationProps }> {
  const searchParams = new URLSearchParams({
    page,
  });
  return fetch(path(`/api/photos?${searchParams}`))
    .then(handleSucceed)
    .catch(handleFailed);
}

route.ts

https://github.com/practical-nextjs-book/applications/blob/main/packages/sns-api-2/src/app/api/photos/route.ts

import { prisma } from "@/lib/prisma";
import type { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = Number(searchParams.get("page") || "1");
  if (isNaN(page) {
    return Response.json({ message: "Invalid Params" }, { status: 400 });
  }
  const photos = await prisma.photo.findMany();
  console.log(
    `GET: /api/photos?${searchParams.toString()} ${new Date().toISOString()}`,
  );
  return Response.json(photos)
}