Closed42

Next.js Tutorial の学び

よしけーよしけー

Next はサーバサイドの React フレームワークであるが、クライアントサイドの SPA で画面遷移を制御する場合は React による制御となる。
このようなフロント側で制御する場合には特別なディレクティブである 'use client'; を宣言する必要がある。

https://nextjs.org/learn/dashboard-app/navigating-between-pages#pattern-showing-active-links

https://nextjs.org/docs/app/building-your-application/rendering/client-components#using-client-components-in-nextjs

よしけーよしけー

request waterfalls の対策1。

Promise.all を使ってパラレルにデータを取得する。

    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);

しかし、特定のリクエストが遅いと引きづられてしまう。

よしけーよしけー

unstable_noStore を使うことで静的レンダリングをオプトアウトして動的レンダリングを強制する?方法がある。

// ...
import { unstable_noStore as noStore } from 'next/cache';
 
export async function fetchRevenue() {
  // Add noStore() here to prevent the response from being cached.
  // This is equivalent to in fetch(..., {cache: 'no-store'}).
  noStore();
 
  // ...
}

https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering#making-the-dashboard-dynamic

[MEMO]
これって api に書く? api 使っている箇所が軒並みに動的レンダリングになる?

[NOTE]
unstable_noStore は実験的な機能、とのこと。

よしけーよしけー

loading.jslayout.js などの特別なファイルは子や孫コンポーネントにも適用される。
が、子や孫に適用させずにカレントディレクトリのみに適用させたいケースがある(かもしれない)。
そんなときは Route Groups を使うことで実現できる。

https://nextjs.org/docs/app/building-your-application/routing/route-groups

下記のチュートリアルでは (overview) ディレクトリを作って、その中に loading.tsxpage.tsx を入れることで loading.tsx の適用対象を page.tsx のみにしている。

https://nextjs.org/learn/dashboard-app/streaming#fixing-the-loading-skeleton-bug-with-route-groups

論理的グループ化する?感じ。

よしけーよしけー

async/await を使ってすべてを手続き型で処理したい。
だが、手続き型で処理すると request waterfalls にハマる。
外部からのデータ取得はパラレルで取得して、ロードできたものから表示したい。
が、 promise の then/catch と useState を使ったリアクティブな制御はしたくない。

そんなときには <Suspense> を使うことでコンポーネントのロードをパラレルに(streaming に?)処理することができる。

        <Suspense fallback={<RevenueChartSkeleton />}>
          <RevenueChart />
        </Suspense>

https://nextjs.org/learn/dashboard-app/streaming#streaming-a-component

[NOTE]
unstable_noStore に依存しているが、 unstable_noStore は実験的な機能、とのこと。

よしけーよしけー

クライアントコンポーネントとサーバーコンポーネント

use client のディレクティブが ある とクライアントコンポーネント。
その場合は制御がクライアントになる。
フォーム入力や、検索条件をブラウザのルーティングに追加するなどの制御はクライアントになる。

use client のディレクティブが ない ならサーバーコンポーネント。
nextjs の場合は基本がこれ?
その場合は nextjs が提供する機能(例えば特別ファイルの page.js とか)が使える。

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination#4-updating-the-table

では useSearchParams のようなクライアント向けの hooks を使うコンポーネントの use client ディレクティブをコメントアウトすると?
-> エラーになる

よしけーよしけー

Server Actions

チュートリアルでは app/lib/actions.ts を Server Actions と呼んでいる。

おそらく Server Actions という概念の、具体的な実装の一部が app/lib/actions.ts を指すものと思われる。

もう少し具体的に言えば下記のような感じ?

  • use server を宣言していて
  • DB 操作といったサーバー側の処理を行うもの
// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    'use server';
 
    // Logic to mutate data...
  }
 
  // Invoke the action using the "action" attribute
  return <form action={create}>...</form>;
}
よしけーよしけー

Zod

TS 向けの検証ライブラリ。

https://zod.dev/

coerce を使うと強制キャストしつつ検証してくれる。

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});

coerce は強制の意。

よしけーよしけー

revalidatePath

引数に設定したパスのキャッシュをクリアする。
クリアするタイミングは、対象のパスにアクセスしたとき。
宣言しないと、データを操作しても表示に反映されず、キャッシュされている古いデータが表示される。

https://nextjs.org/docs/app/api-reference/functions/revalidatePath

削除時の動作がわかりやすい。
revalidatePath をコメントアウトすると、削除しても画面に反映されなくなる。

https://nextjs.org/learn/dashboard-app/mutating-data#deleting-an-invoice

よしけーよしけー

next/navigation が提供する redirect は例外を投げるので try/catch を使う場合は注意が必要。
(catch (error) すると拾ってしまう)

よしけーよしけー

useFormState

react が提供するフック。
ただし 202404 現在では実験的な機能。

https://ja.react.dev/reference/react-dom/hooks/useFormState

hook なので client side 向け。

const [state, dispatch] = useFormState(createInvoice, initialState);
  • createInvoice : server action の実処理。 FormData 型の入力値を受け取りバリチェ、 DB 操作を行う。返り値は state に設定される
  • initialState : state に設定される初期値
  • state : 初期値は initialState で、 action 処理後は action の返り値になる
  • dispatch : <form action={action}> のように form タグ設定用に制御が加えられたメソッド

https://nextjs.org/learn/dashboard-app/improving-accessibility#form-validation

よしけーよしけー

useFormState を使うとなると server action 内で例外を飛ばすと意図しない振る舞いが発生する。(チュートリアル的にいうと zod のバリチェ)
このため zod のバリチェは safeParse を使って検証と検証結果のチェックを別々に処理するとよい。

  // 検証
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  // 検証結果のチェック
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Missing Fields. Failed to Create Invoice.',
    };
  }

  // OK ならば検証結果から入力値を取得
  const { customerId, amount, status } = validatedFields.data;
よしけーよしけー

auth.config.ts

認証用のコンフィグファイル。
nextjs や nextauth が提供しているファイルではない。
プロジェクトとしての 決め のファイルなのでファイル名やパスは任意でいいはず。。
(チュートリアルでは middleware.ts で NextAuth を初期化しているので、そこで書いてもいいはず)

今回は、認証はプロジェクトとしてユニークになるのでルートに置いている気がする。

NextAuth.js の設定オプションが含まれる。

よしけーよしけー

bcrypt は Next.js ミドルウェアでは利用できない Node.js API に依存している。

よしけーよしけー

auth.ts

auth.config.ts 拡張用(?)のファイル。
nextjs や nextauth が提供しているファイルではない。
プロジェクトとしての 決め のファイルなのでファイル名やパスは任意でいいはず。。

https://authjs.dev/getting-started/installation#configure

auth.config.ts と別にする理由はよくわらん。
ただ auth.config.ts は middleware.ts でも使っている。
認証処理に関するドメインと、ミドルウェアに関するドメインでドメインが異なるのでファイルを分けた?

認証処理自体は auth.ts に書かれている。

  • zod による入力値の検証
  • ユーザーの取得
  • ユーザーがあればユーザー情報を返す、なければ null を返す

https://authjs.dev/reference/core/providers/credentials#authorize

NextAuth() による初期化後に返るオブジェクトは NextAuthResult 。

https://authjs.dev/reference/next-auth#nextauthresult

signIn, signOut はメソッドで、サインインやサインアウト処理を呼び出す際に使用する。

よしけーよしけー

useFormStatus

useFormStatus は、直近のフォーム送信に関するステータス情報を提供するフックです。

function LoginButton() {
  const { pending } = useFormStatus();
 
  return (
    <Button className="mt-4 w-full" aria-disabled={pending}>
      Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
    </Button>
  );
}

pending が送信状態を boolean で返すので、送信状態で有効/無効を切り替えている。

https://ja.react.dev/reference/react-dom/hooks/useFormStatus

よしけーよしけー

favicon.icoopengraph-image.jpg/app 下に置くと自動的に反映される。

よしけーよしけー

layout.tsx で下記のメタデータを metadata: Metadata として宣言して export すると自動的に反映される。

  • title
  • description
  • metadataBase

また page.tsx で上書きすると個別に設定可能。

layout.tsx で次のような書き方で埋め込み式に書くことも可能。

  title: {
    template: '%s | Acme Dashboard',
    default: 'Acme Dashboard',
  },

page.tsx に宣言があると template が、宣言がなければ default が適用される。

https://nextjs.org/docs/app/api-reference/functions/generate-metadata

よしけーよしけー

最終的なディレクトリ構成

.
|-- app
|   |-- dashboard
|   |   |-- (overview)
|   |   |   |-- loading.tsx
|   |   |   `-- page.tsx
|   |   |-- customers
|   |   |   `-- page.tsx
|   |   |-- invoices
|   |   |   |-- [id]
|   |   |   |   `-- edit
|   |   |   |       |-- not-found.tsx
|   |   |   |       `-- page.tsx
|   |   |   |-- create
|   |   |   |   `-- page.tsx
|   |   |   |-- error.tsx
|   |   |   `-- page.tsx
|   |   `-- layout.tsx
|   |-- favicon.ico
|   |-- layout.tsx
|   |-- lib
|   |   |-- actions.ts
|   |   |-- data.ts
|   |   |-- definitions.ts
|   |   |-- placeholder-data.js
|   |   `-- utils.ts
|   |-- login
|   |   `-- page.tsx
|   |-- opengraph-image.png
|   |-- page.tsx
|   `-- ui
|       |-- acme-logo.tsx
|       |-- button.tsx
|       |-- customers
|       |   `-- table.tsx
|       |-- dashboard
|       |   |-- cards.tsx
|       |   |-- latest-invoices.tsx
|       |   |-- nav-links.tsx
|       |   |-- revenue-chart.tsx
|       |   `-- sidenav.tsx
|       |-- fonts.ts
|       |-- global.css
|       |-- invoices
|       |   |-- breadcrumbs.tsx
|       |   |-- buttons.tsx
|       |   |-- create-form.tsx
|       |   |-- edit-form.tsx
|       |   |-- pagination.tsx
|       |   |-- status.tsx
|       |   `-- table.tsx
|       |-- login-form.tsx
|       |-- search.tsx
|       `-- skeletons.tsx
|-- auth.config.ts
|-- auth.ts
|-- middleware.ts
|-- next-env.d.ts
|-- next.config.js
|-- package-lock.json
|-- package.json
|-- postcss.config.js
|-- prettier.config.js
|-- public
|   |-- customers
|   |   |-- amy-burns.png
|   |   |-- balazs-orban.png
|   |   |-- delba-de-oliveira.png
|   |   |-- emil-kowalski.png
|   |   |-- evil-rabbit.png
|   |   |-- guillermo-rauch.png
|   |   |-- hector-simpson.png
|   |   |-- jared-palmer.png
|   |   |-- lee-robinson.png
|   |   |-- michael-novotny.png
|   |   |-- steph-dietz.png
|   |   `-- steven-tey.png
|   |-- hero-desktop.png
|   `-- hero-mobile.png
|-- scripts
|   `-- seed.js
|-- tailwind.config.ts
`-- tsconfig.json
よしけーよしけー

use client を適用しているコンポーネント

file path row num note
app/dashboard/invoices/error.tsx 1 invoices 内のサーバサイド(?)で発生する error をフックする特別なファイル。クライアント側である必要があるとのこと
app/ui/dashboard/nav-links.tsx 1 サイドバーやSP幅時のヘッダメニュー。静的に表示後はフロント側で動的に制御(ハイライトとか)したいのでクライアント側
app/ui/invoices/create-form.tsx 1 新規登録のフォーム。入力の動的制御やサーバへのリクエストなどはクライアント側の制御
app/ui/invoices/edit-form.tsx 1 編集のフォーム。入力の動的制御やサーバへのリクエストなどはクライアント側の制御
app/ui/invoices/pagination.tsx 1 ページネーション。一覧の再読込などはクライアント側で動的に制御するのが一般的(サーバ制御で再度全読み込みしてもいいけど)
app/ui/login-form.tsx 1 ログインフォーム
app/ui/search.tsx 1 検索
よしけーよしけー

use server を適用しているコンポーネント

file path row num note
app/lib/actions.ts 1 actions に書かれているのは sql. なのでサーバ側で動くことが前提
app/ui/dashboard/sidenav.tsx 23 サインアウト処理に宣言されている部分適用。サインアウト処理はサーバ側で処理するので宣言が必要?
よしけーよしけー

use client ディレクティブは、サーバー側で生成する静的なコンポーネント内に、クライアント側で制御したい動的なコンポーネントを含めたい場合に宣言する感じ

なので例えば page.tsxuse client が宣言されることはないはず。
あくまでも部分的なコンポーネントに宣言される。
(ページをまるっと動的にしたい場合は宣言するかも?)

https://nextjs.org/learn/dashboard-app/partial-prerendering#what-is-partial-prerendering

よしけーよしけー

同期系の Page

file path row num code note
app/dashboard/customers/page.tsx 1 export default function Page() { 未実装のカスタマページ
app/page.tsx 7 export default function Page() { 静的なトップページ

非同期系の Page

file path row num code note
app/dashboard/(overview)/page.tsx 8 export default async function Page() { ダッシュボードのトップコンポーネント。 Suspense で非同期にデータ取得している
app/dashboard/invoices/[id]/edit/page.tsx 6 export default async function Page({ params }: { params: { id: string } }) { インボイスの編集ページ。内部で await で DB アクセスあり
app/dashboard/invoices/create/page.tsx 5 export default async function Page() { インボイスの登録ページ。内部で await で DB アクセスあり
app/dashboard/invoices/page.tsx 15 export default async function Page({ インボイスの一覧ページ。内部で await で DB アクセスあり
このスクラップは8時間前にクローズされました