## 概要と目的
このブログ記事では、実際のシナリオで必要となる最も重要な Next.js 機能について説明したいと思います。
このブログ記事は、私自身と興味のある読者のための単一の参考資料として作成しました。 nextjs ドキュメント全体を読む必要はありません。 nextjs の重要な 実用的な機能をすべて備えた凝縮されたブログ記事を作成し、定期的にアクセスして知識を更新することが容易になると思います。
メモ アプリケーションを並行して構築しながら、以下の機能を一緒に見ていきます。
アプリルーター
ロードとエラー処理
サーバーアクション
データのフェッチとキャッシュ
ストリーミングとサスペンス
並行ルート
エラー処理
アプリケーション コードを記録する最終的なメモは次のようになります:
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
この Github リポジトリの spthacode にある最終コードに直接ジャンプしてください。
それでは、これ以上面倒なことはせずに、始めましょう!
メモ アプリケーションの開発に入る前に、先に進む前に知っておくべき重要な nextjs の概念をいくつか紹介したいと思います。
App Router は、従来の "/page" ディレクトリでは不可能だった多くの機能をサポートする新しいディレクトリ "/app" です。 :
ネストされたルーティング: フォルダーをフォルダー内にネストできます。ページ パス URL は、同じフォルダーのネストに従います。たとえば、[noteId] 動的パラメータが /app/notes/[noteId]/edit/page.tsx >"1" は "/notes/1/edit.
/app Router に触れる前に、誰もがマスターすべき非常に重要なトピックに飛び込んでみましょう。
サーバーコンポーネントサーバー コンポーネントは、基本的にサーバー上でレンダリングされるコンポーネントです。
"use client" ディレクティブが前に付いていないコンポーネントは、デフォルトでページやレイアウトを含むサーバー コンポーネントになります。
サーバーコンポーネントは、任意のnodejs API、またはサーバー上で使用される任意のコンポーネントと対話できます。
クライアント コンポーネントとは異なり、サーバー コンポーネントの前に async キーワードを付けることができます。したがって、任意の非同期関数を呼び出して、コンポーネントをレンダリングする前にそれを待つことができます。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
その答えは、
SEO、パフォーマンスとユーザー エクスペリエンスという短い言葉で要約できます。
ユーザーがページにアクセスすると、ブラウザは HTML、CSS、JavaScript などの Web サイト資産をダウンロードします。JavaScript バンドル (フレームワーク コードを含む) は、サイズが大きいため、他のアセットのロードよりも時間がかかります。
クローラーにも同じことが当てはまります。
LCP、TTFB、直帰率など、他の多くの SEO 指標が影響を受けます。
クライアント コンポーネントは、単にユーザーのブラウザに送信されるコンポーネントです。
クライアント コンポーネント は、単なる裸の HTML コンポーネントや CSS コンポーネントではありません。これらが動作するには対話性が必要なので、サーバー上でレンダリングすることは実際には不可能です。
対話性は、反応 (useState、useEffect) などの JavaScript フレームワーク、ブラウザーのみ、または DOM API のいずれかによって保証されます。
クライアントコンポーネント宣言の前に、"use client" ディレクティブを置く必要があります。これにより、Nextjs はインタラクティブな部分 (useState、useEffect...) を無視し、ユーザーのブラウザに直接送信するように指示されます。
/client-component.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
Nextjs で最もイライラするのは、サーバー コンポーネント と クライアント コンポーネント の間のネストのルールを怠ると遭遇する可能性がある奇妙なバグです。
次のセクションでは、サーバー コンポーネント と クライアント コンポーネント の間で考えられるさまざまなネストの並べ替えを紹介することで、そのことを明確にします。
これら 2 つの並べ替えは明らかに許可されているためスキップします: クライアント コンポーネントと別のクライアント コンポーネント、および別のサーバー コンポーネント内のサーバー コンポーネント。
クライアント コンポーネント をインポートし、サーバー コンポーネント内で通常どおりレンダリングできます。 ページ と レイアウト はデフォルトでサーバー コンポーネントであるため、この順列はある程度明白です。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
クライアント コンポーネントをユーザーのブラウザに送信し、その内部にあるサーバー コンポーネントがデータをレンダリングしてフェッチするのを待つことを想像してください。サーバー コンポーネントはすでにクライアントに送信されているため、それは不可能です。その後、サーバー上でどのようにレンダリングできますか?
このタイプの順列が Nextjs でサポートされていないのはそのためです。
そのため、クライアント コンポーネント 内に サーバー コンポーネント をインポートして子としてレンダリングすることは避けてください。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
jsx ツリー内のクライアント コンポーネントをプッシュダウンして、ユーザーのブラウザに送信される JavaScript を減らすように常に努めてください。
サーバーコンポーネントをクライアントコンポーネントの子として直接インポートしてレンダリングすることはできませんが、react composabilityの性質.
その秘訣は、上位レベルのサーバー コンポーネント (ParentServerComponent) で サーバー コンポーネント を クライアント コンポーネント の子として渡すことです。
それをパパトリック と呼びましょう :D.
このトリックにより、クライアント コンポーネントをユーザーのブラウザに送信する前に、渡されたサーバー コンポーネントがサーバーでレンダリングされることが保証されます。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/page.tsx ホーム ページで具体的な例を確認します。
ここでは、クライアント コンポーネント内の子として渡されるサーバー コンポーネントをレンダリングします。クライアント コンポーネントは、ブール状態変数の値に応じて、サーバー コンポーネントのレンダリングされたコンテンツを条件付きで表示または非表示にできます。
サーバー アクション は、クライアント側コンポーネントからサーバー上で宣言された関数を リモート かつ 安全に 呼び出すことを可能にする興味深い nextjs 機能です。 .
サーバー アクションを宣言するには、以下に示すように "use server" ディレクティブを関数の本体に追加するだけです。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
"use server" ディレクティブは、関数にサーバー上でのみ実行されるサーバー側のコードが含まれていることを Nextjs に伝えます。
内部では、Nextjs は アクション ID を送信し、このアクション用に予約されたエンドポイントを作成します。
クライアント コンポーネント でこのアクションを呼び出すと、Nextjs は アクション ID で識別されるアクションの一意のエンドポイントに対して POST リクエストを実行し、 リクエストボディでアクションを呼び出すときに渡したシリアル化された引数。
この単純化した例でそれを明確にしましょう。
サーバー アクションを宣言するには、関数本体ディレクティブで "use server" を使用する必要があることを前に説明しました。しかし、一度に多数のサーバー アクションを宣言する必要がある場合はどうなるでしょうか。
以下のコードに示すように、ディレクティブをヘッダーまたはファイルの先頭で使用するだけです。
/server/actions.ts
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
サーバーアクションは常に非同期としてマークする必要があることに注意してください
そのため、上記のコードでは、createLogAction.
このアクションは、サーバー上の /logs ディレクトリの下の特定のファイルにログ エントリを保存します。
ファイルの名前は、name アクション引数に基づいて付けられます。
アクション 作成日と message アクション引数で構成されるログ エントリを追加します。
ここで、CreateLogButton クライアント側コンポーネントで作成したアクションを使用してみましょう。
/components/CreateLogButton.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
ボタン コンポーネントは、アクションが実行されているかどうかを追跡するために使用される isSubmitting という名前のローカル状態変数を宣言しています。アクションが実行されると、ボタンのテキストが "Log Button" から "Loading..." に変わります。
サーバー アクションは、ログ ボタンコンポーネントをクリックすると呼び出されます。
まず、Note 検証スキーマとタイプを作成することから始めましょう。
モデルはデータ検証を処理することになっているため、その目的のために zod と呼ばれる一般的なライブラリを使用します。
zod の優れている点は、モデルの定義と対応する TypeScript の生成をシームレスなタスクにする、わかりやすい API です。
メモには派手で複雑なモデルは使用しません。各メモには一意の ID、タイトル、内容、作成日フィールドがあります。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
InsertNoteSchema や WhereNoteSchema などの便利な追加スキーマも宣言しています。これにより、後でモデルを操作する再利用可能な関数を作成するときに作業が楽になります。
メモはメモリに保存され、操作されます。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
notes 定数がファイルにインポートされる (ページのリロードなど) たびに配列の状態が失われないように、notes 配列をグローバルのこのオブジェクトに保存しています。
createNote ユースケースでは、ノート配列にノートを挿入できます。 notes.unshift メソッドは、要素を配列の末尾ではなく先頭にプッシュするため、notes.push メソッドの逆であると考えてください。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
updateNote を使用して、指定された ID のメモ配列内の特定のメモを更新します。まず要素のインデックスを検索し、見つからない場合はエラーをスローし、見つかったインデックスに基づいて対応するメモを返します。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
deleteNote ユースケース関数は、指定されたノート ID を削除するために使用されます。
このメソッドは同様に動作します。まず、指定された ID でノートのインデックスを検索し、見つからない場合はエラーをスローして、見つかった ID によってインデックス付けされた対応するノートを返します。
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
getNote 関数は一目瞭然で、指定された ID のノートを検索するだけです。
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
メモ データベース全体をクライアント側にプッシュしたくないため、利用可能なメモの合計の一部のみを取得します。したがって、サーバー側のページネーションを実装する必要があります。
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
したがって、getNotes 関数を使用すると、基本的に page 引数を渡すことでサーバーから特定のページを取得できます。
limit 引数は、特定のページに存在する項目の数を決定するために使用されます。
例:
notes 配列に 100 個の要素が含まれており、limit 引数が 10 に等しい場合。
サーバーからページ 1 をリクエストすると、最初の 10 項目のみが返されます。
search 引数は、サーバー側の検索を実装するために使用されます。これは、タイトルまたはコンテンツ属性のいずれかに部分文字列として search 文字列を持つメモのみを返すようにサーバーに指示します。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
このユースケースは、ユーザーの最近のアクティビティに関する偽のデータを取得するために使用されます。
この関数は /dashboard ページで使用します。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
このユースケース関数は、メモ (#something) で使用されているさまざまなタグに関する統計を取得する役割を果たします。
この関数は /dashboard ページで使用します。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
このユースケース関数を使用して、名前やメールアドレスなどの一部のユーザー情報に関する偽のデータを返すだけです。
この関数は /dashboard ページで使用します。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
このホームページでは、クライアント コンポーネント 内で サーバー コンポーネント をレンダリングするための以前のトリックまたは回避策をデモします (PaPa トリック :D) .
/app/page.tsx
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
上記のコードでは、アプリケーション内の "/" ページのレンダリングを担当する Home という 親サーバー コンポーネント を宣言しています。
RandomNote という名前の サーバー コンポーネント と NoteOfTheDay という名前の クライアント コンポーネント をインポートしています。
RandomNote サーバー コンポーネントを子として NoteOfTheDay クライアント サイド コンポーネントに渡します。
/app/components/RandomNote.ts
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
RandomNote サーバー コンポーネントは次のように動作します:
getRandomNote ユースケース関数を使用してランダムなノートを取得します。
タイトルとメモ全体の内容の一部または部分文字列で構成されるメモの詳細をレンダリングします。
/app/components/NoteOfTheDay.ts
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
反対側の NoteOfTheDay クライアント コンポーネントは以下のように動作します。
/app/notes/page.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
まず、以下を担当するサーバー コンポーネントである /app/notes/page.tsx ページを作成します。
URL の末尾の ? マークの後に付加される文字列であるページ検索パラメーターを取得します: http://localhost:3000/notes?page=1&search=Something
ローカルで宣言された関数 fetchNotes に検索パラメータを渡します。
fetchNotes 関数は、以前に宣言したユースケース関数 getNotes を使用して、現在のノート ページを取得します。
getNotes 関数を、"next/cache" からインポートされた unstable_cache というユーティリティ関数でラップしていることがわかります。不安定キャッシュ関数は、getNotes 関数からの応答をキャッシュするために使用されます。
データベースにメモが追加されていないことが確実な場合。ページがリロードされるたびにこれを押すのは意味がありません。したがって、unstable_cache 関数は、getNotes 関数の結果に "notes" タグを付けています。このタグは、後で "notes" メモが追加または削除された場合はキャッシュされます。
fetchNotes 関数は、ノートと合計の 2 つの値を返します。
NotesList と呼ばれる クライアント側コンポーネント に渡されます。
この問題を解決するには、Nextjs という素晴らしい機能を利用します。
サーバーサイドページストリーミング.
/app/notes/page.tsx ファイルの隣に loading.tsx ファイルを作成します。
/app/notes/loading.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
それはクールですね:)。 loading.tsx ファイルを作成するだけで完了です。あなたの ux は次のレベルまで成長しています。
/app/notes/components/NotesList.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
メモ リスト クライアント側コンポーネント 親 サーバー コンポーネント (NotesPage) からメモとページネーション関連のデータを受け取ります。
次に、コンポーネントはノートの現在のページのレンダリングを処理します。個々のノート カードはすべて、NoteView コンポーネントを使用してレンダリングされます。
また、Next.js Link コンポーネントを使用して前後のページへのリンクも提供します。これは、シームレスで高速なクライアントを実現するために次のページと前のページのデータをプリフェッチするために不可欠です。 -サイドナビゲーション。
サーバーサイド検索を処理するために、useNotesSearchというカスタムフックを使用しています。これは基本的に、ユーザーが検索Input.
/app/notes/components/NoteView.ts
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
NoteView コンポーネントは単純で、対応するタイトル、内容の一部、およびメモの詳細を表示または編集するためのアクション リンクを含む個々のメモ カードをレンダリングすることのみを担当します。
/app/notes/components/hooks/use-notes-search.ts
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
useNotesSearch カスタム フックは次のように機能します:
useState フックを使用して、initialSearch プロパティをローカル状態に保存します。
useEffect React フックを使用して、currentPage または debouncedSearchValue 変数の値が変更されるたびにページ ナビゲーションをトリガーします。
setSearch 関数は、ユーザーが検索入力に何かを入力するときに文字が変更されるたびに呼び出されます。これにより、短時間でナビゲーションが多すぎます。
CreateNoteForm クライアント コンポーネントのサーバー コンポーネント ラッパーである /app/notes/create/page.tsx を見てみましょう。
/app/notes/create/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/notes/create/components/CreateNoteForm.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
CreateNoteForm クライアント コンポーネント フォームは、ユーザーからデータを取得し、それをローカル状態変数 (タイトル、コンテンツ) に保存する役割を果たします。
送信ボタンをクリックした後にフォームが送信されると、createNoteActionがtitleとcontentのローカル状態引数を使用して送信されます。 .
isSubmitting 状態ブール変数は、アクションの送信ステータスを追跡するために使用されます。
createNoteAction がエラーなく正常に送信された場合、ユーザーは /notes ページにリダイレクトされます。
/app/notes/create/actions/create-note.action.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
createNoteAction アクション コードは単純で、含まれるファイルの前に "use server" ディレクティブがあり、このアクションがクライアント コンポーネントで呼び出し可能であることを Next.js に示します。
サーバー アクションについて強調すべき点の 1 つは、アクション インターフェイスのみがクライアントに送信され、アクション自体内のコードは送信されないということです。
言い換えると、アクション内のコードはサーバー上に存在するため、クライアントからサーバーに送られる入力を信頼すべきではありません。
そのため、ここでは zod を使用して、以前に作成したスキーマを使用して rawNote アクション引数を検証しています。
入力を検証した後、検証されたデータを使用して createNote ユースケースを呼び出します。
ノートが正常に作成されると、revalidateTag 関数が呼び出され、"notes" としてタグ付けされたキャッシュ エントリが無効になります (unstable_cache 関数を思い出してください)これは /notes ページで使用されます)。
ノートの詳細ページには、一意の ID が与えられた特定のノートのタイトルと完全な内容が表示されます。それに加えて、メモを編集または削除するためのいくつかのアクション ボタンが表示されます。
/app/notes/[noteId]/page.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
まず、ページ プロパティからページ パラメータを取得します。 Next.js 13 では、params ページ引数は Promise なので待機する必要があります。
それを行った後、params.noteId をローカルで宣言された関数 fetchNote に渡します。
/app/notes/[noteId]/fetchers/fetch-note.ts
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
fetchNote 関数は、getNote ユースケースを unstable_cache でラップし、返された結果を "note-details" および note-details/${id} タグ。
"note-details" タグを使用すると、すべてのメモ詳細キャッシュ エントリを一度に無効にすることができます。
note-details/${id} タグは、一意の ID で定義された特定のメモにのみ関連付けられます。したがって、これを使用して、ノートのセット全体ではなく、特定のノートのキャッシュ エントリを無効にすることができます。
/app/notes/[noteId]/loading.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
リマインダー
loading.tsx は、ノートの詳細ページがサーバーでデータを取得している間にレンダリングされる特別な Next.js ページです。
言い換えると、fetchNote 関数の実行中に、空白の画面の代わりにスケルトン ページがユーザーに表示されます。
この nextjs 機能はページ ストリーミング と呼ばれます。これにより、コンテンツを段階的にストリーミングしながら、動的ページの静的な親レイアウト全体を送信できます。
これにより、ページの動的コンテンツがサーバー上で取得されている間に UI がブロックされることが回避され、パフォーマンスとユーザー エクスペリエンスが向上します。
/app/notes/[noteId]/components/DeleteNoteButton.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
DeleteNoteButton クライアント側コンポーネントを見てみましょう。
このコンポーネントは、削除ボタンをレンダリングし、
deleteNoteAction を実行し、アクションが正常に実行されるとユーザーを /notes ページにリダイレクトします。
アクションの実行ステータスを追跡するために、ローカル状態変数isDeleting を使用しています。
/app/notes/[noteId]/actions/delete-note.action.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
deleteNoteAction コードは次のように機能します:
/app/notes/[noteId]/edit/page.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
/app/notes/[noteId]/edit/page.tsx ページは、params Promise から noteId パラメータを取得するサーバー コンポーネントです。
次に、fetchNote 関数を使用してノートを取得します。
フェッチが成功した後。メモを EditNoteForm クライアント側コンポーネントに渡します。
/app/notes/[noteId]/edit/components/EditNoteForm.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
EditNoteForm クライアント側コンポーネントはメモを受け取り、ユーザーがメモの詳細を更新できるフォームをレンダリングします。
title および content ローカル状態変数は、対応する入力またはテキストエリアの値を保存するために使用されます。
メモを更新 ボタンを介してフォームが送信されたとき。 updateNoteAction は、title と content の値を引数として使用して呼び出されます。
isSubmitting 状態変数は、アクションの送信ステータスを追跡するために使用され、アクションの実行時に読み込みインジケーターを表示できます。
/app/notes/[noteId]/edit/actions/edit-note.action.ts
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
updateNoteAction アクションは次のように機能します:
/app/dashboard/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/dashboard/page.tsx ページは、Notessummary、RecentActivity、および TagCloud.
各サーバー コンポーネントは独自のデータを個別に取得します。各サーバー コンポーネントは React
Suspense 境界でラップされます。
サスペンス境界の役割は、子サーバー コンポーネントが独自のデータをフェッチしているときに、フォールバック コンポーネント (この場合はスケルトン) を表示することです。
別の言い方をすると、サスペンス 境界により、何らかの条件が満たされる (子の内部のデータがロードされる) まで、子のレンダリングを延期または遅らせることができます。
そのため、ユーザーはページを多数のスケルトンの組み合わせとして見ることができます。すべての個々のコンポーネントの応答がサーバーによってストリーミングされている間。このアプローチの主な利点の 1 つは、1 つ以上のサーバー コンポーネントが他のコンポーネントに比べて時間がかかる場合に、UI のブロックを回避できることです。
各コンポーネントの個々のフェッチ時間が次のように分布すると仮定します。
更新をクリックすると、最初に 3 つのスケルトン ローダーが表示されます。
1 秒後に RecentActivity コンポーネントが表示されます。
2 秒後、Notessummary が続き、次に TagCloud が続きます。
つまり、コンテンツを表示する前にユーザーを 3 秒待たせるのではなく。 RecentActivity を最初に表示することで、時間を 2 秒短縮しました。
このインクリメンタル レンダリング アプローチにより、ユーザー エクスペリエンスとパフォーマンスが向上します。
個々の サーバー コンポーネント のコードが以下で強調表示されています。
/app/dashboard/components/RecentActivity.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
RecentActivity サーバー コンポーネントは、基本的に getRecentActivity ユースケース関数を使用して最後のアクティビティを取得し、順序なしリストでレンダリングします。
/app/dashboard/components/TagCloud.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
TagCloud サーバー側コンポーネントは、ノートのコンテンツで使用されているすべてのタグ名をそれぞれの数とともに取得してレンダリングします。
/app/dashboard/components/Notessummary.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
Notessummary サーバー コンポーネントは、getNotesummary ユースケース関数を使用して概要情報を取得した後、それをレンダリングします。
次に、プロフィール ページに進み、Parallel Routes と呼ばれる興味深い nextjs 機能を説明します。
並列ルートにより、同時にまたは条件付きで同じレイアウト内で1つ以上のページをレンダリングできます。
以下の例では、ユーザー情報ページとユーザーノートページを/app/profileという同じレイアウト内にレンダリングします。 .
名前付きスロットを使用して、並列ルートを作成できます。名前付きスロットはサブ ページとして正確に宣言されますが、通常のページとは異なり、フォルダー名の前に @ 記号を付ける必要があります。
たとえば、/app/profile/ フォルダー内に 2 つの名前付きスロットを作成します。
次に、/profile ページのレイアウトを定義するレイアウト ファイル /app/profile/layout.tsx ファイルを作成しましょう。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
上記のコードからわかるように、@info ページと @notes ページ内のコンテンツを含む info パラメータと notes パラメータにアクセスできるようになりました。
したがって、@info ページは左側にレンダリングされ、@notes は右側にレンダリングされます。
page.tsx のコンテンツ (children によって参照される) は、ページの下部にレンダリングされます。
/app/profile/@info/page.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
UserInfoPage は、getUserInfo ユースケース関数を使用してユーザー情報を取得するサーバー コンポーネントです。
上記のフォールバック スケルトンは、コンポーネントがデータを取得してサーバー上でレンダリングされるときにユーザーのブラウザーに送信されます (サーバー サイド ストリーミング)。
/app/profile/@info/loading.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
同じことが LastNotesPage サーバー側コンポーネントにも当てはまります。スケルトン UI がユーザーに表示されている間に、データをフェッチしてサーバー上でレンダリングします
/app/profile/@notes/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/profile/@notes/loading.tsx
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
次に、Nextjs の非常に優れた機能である error.tsx ページを見てみましょう。
アプリケーションを運用環境にデプロイするとき、ページの 1 つから捕捉されなかったエラーがスローされたときに、ユーザー フレンドリーなエラーを表示したいと思うでしょう。
ここで、error.tsx ファイルが登場します。
まず、数秒後にキャッチされないエラーをスローするサンプル ページを作成しましょう。
/app/error-page/page.tsx
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
ページがスリープしているとき、またはスリープ関数の実行を待機しているとき。以下の読み込みページがユーザーに表示されます。
/app/error-page/loading.tsx
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
数秒後にエラーがスローされ、ページが削除されます:(.
これを避けるために、/app/error-page/page の エラー境界 として機能するコンポーネントをエクスポートする error.tsx ファイルを作成します。 .tsx.
/app/error-page/error.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
このガイドでは、実用的なメモ アプリケーションを構築することによって、Next.js の主要な機能を検討しました。以下について説明しました:
これらの概念を現実世界のプロジェクトに適用することで、私たちは Next.js の強力な機能を実際に体験することができました。理解を確実にするための最良の方法は実践することであることを忘れないでください。
ご質問がある場合、またはさらに話し合いたい場合は、お気軽にここからご連絡ください。
コーディングを楽しんでください!
以上がNext.js の詳細: 高度な機能を備えた Notes アプリの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。