Next14のLearn on Bun
Node環境ではなく、Bun環境でチュートリアルを行う。
公式の以下コマンドでプロジェクトを作成する。
// これじゃない
bun create next-app
以下のコマンドでプロジェクトを作成する。
bun create next-app nextjs-dashboard --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-exam
ple"
今後のことを考えて、プロジェクトはモノレポで作成したい。
どうも、ルートから各ワークスペースのスクリプトを実行する手段はなさそう。
bun.lockbをテキストで表示すると文字化けするから、なぜ?っと調べたら、
どうやら、bun.lockbはバイナリだった。
なぜバイナリなのでしょうか?
一言で言えば、パフォーマンスです。Bun のロックファイルは、保存とロードが信じられないほど速く、通常のロックファイル内にあるものよりもはるかに多くのデータを保存します。
とりあえず、Learnを進める。
ルートでbun install
しようとしたら、エラーが出た。
bun install v1.0.21 (837cbd60)
error: Missing "name" from package.json in packages//nextjs-dashboard
at /home/bun/app/package.json
どうやら、nextjs-dashboardのpackage.jsonにnameがないことが原因。
なので、package.jsonに"name": "@tutorial-next14/nextjs-dashboard"
を追記。
次は以下のエラーが発生した。
bun install v1.0.21 (837cbd60)
🔒 Saving lockfile... error: failed to save lockfile: EBUSY
ルートのbun.lockbを削除して、再度実行してもダメだった。
docker-composeのボリュームのマウントの仕方だめだった。
volumes:
- ./node_modules:/home/bun/app/node_modules/
- ./packages:/home/bun/app/packages/
- ./bun.lockb:/home/bun/app/bun.lockb
- ./package.json:/home/bun/app/package.json
- ./tsconfig.json:/home/bun/app/tsconfig.json
↓に修正。
volumes:
- .:/home/bun/app
次は以下のエラーが発生したけど、再度bun install
したらエラーが発生しなくなっている。なぜ?
bun install v1.0.21 (837cbd60)
⚙️ bcrypt [1/3] tutorial-next14 package.json is not node-pre-gyp ready:
package.json must declare these properties:
main
version
binary
node-pre-gyp info it worked if it ends with ok
node-pre-gyp info using bun@1.0.11
node-pre-gyp info using node@20.8.0 | linux | arm64
node-pre-gyp ERR! install error
node-pre-gyp ERR! stack Error: tutorial-next14 package.json is not node-pre-gyp ready:
node-pre-gyp ERR! stack package.json must declare these properties:
node-pre-gyp ERR! stack main
node-pre-gyp ERR! stack version
node-pre-gyp ERR! stack binary
node-pre-gyp ERR! stack at validate_config (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/util/versioning.js:221:9)
node-pre-gyp ERR! stack at <anonymous> (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/util/versioning.js:170:45)
node-pre-gyp ERR! stack at install (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/install.js:95:35)
node-pre-gyp ERR! stack at <anonymous> (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/node-pre-gyp.js:7:79)
node-pre-gyp ERR! stack at run (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/main.js:10:32)
node-pre-gyp ERR! stack at <anonymous> (/home/bun/app/node_modules/@mapbox/node-pre-gyp/lib/main.js:63:6)
node-pre-gyp ERR! stack at anonymous (native)
node-pre-gyp ERR! stack at <anonymous> (/home/bun/app/node_modules/@mapbox/node-pre-gyp/bin/node-pre-gyp:2:10)
node-pre-gyp ERR! stack at <anonymous> (:11:43)
node-pre-gyp ERR! System Linux 6.6.12-linuxkit
node-pre-gyp ERR! command "/usr/local/bin/bun" "/home/bun/app/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build"
node-pre-gyp ERR! cwd /home/bun/app
node-pre-gyp ERR! node -v v20.8.0
node-pre-gyp ERR! bun -v v1.0.11
node-pre-gyp ERR! not ok
error: install script from "bcrypt" exited with 1
↓再度実行すると。
bun install v1.0.21 (837cbd60)
+ @tutorial-next14/nextjs-dashboard@workspace:packages/nextjs-dashboard
1 package installed [151.00ms]
ファイル構造は以下のようだ。
/app: アプリケーションのすべてのルート、コンポーネント、ロジックが含まれています。主にここから作業します。
/app/lib: 再利用可能なユーティリティ関数やデータ取得関数など、アプリケーションで使用される関数が含まれます。
/app/ui: カード、テーブル、フォームなど、アプリケーションのすべての UI コンポーネントが含まれます。時間を節約するために、これらのコンポーネントは事前にスタイル設定されています。
/public: 画像など、アプリケーションのすべての静的アセットが含まれます。
/scripts: 後の章でデータベースにデータを設定するために使用するシード スクリプトが含まれています。
設定ファイルnext.config.js:アプリケーションのルートなどに設定ファイルがあることにも気づきます。これらのファイルのほとんどは、 を使用して新しいプロジェクトを開始するときに作成され、事前設定されますcreate-next-app。このコースではそれらを変更する必要はありません。
将来的に、libとui、scriptsは別パッケージとして切り出したいな。
【TODO】プラグイン周りについては、後でちゃんと読んでおく必要がありそうだな。
以下のエラーが出た。
https://nextjs.org/docs/messages/module-not-found
⚠ Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/messages/fast-refresh-reload
どうやら、import '@/app/ui/gloabal.css';
としていた。
訂正し、import '@/app/ui/global.css';
としたけど、エラーが治らず。
先のエラーは、ビルド時に以下のようなことが原因で発生するらしいが、それとは別に原因がありそう。けど、原因不明。
高速リフレッシュでは、ファイルを編集するときに完全なリロードを実行する必要がありました。次のような理由が考えられます。
編集しているファイルには、React コンポーネントに加えて他のエクスポートが含まれている場合があります。
React コンポーネントは匿名関数です。
コンポーネント名は PascalCase ではなく CamelCase で表されます (たとえば、textFieldの代わりにTextField)。
調べているうちに、.next
フォルダを削除して、ビルドしなおせば治るとのこと。
早速試したら、治った。結局、原因がわからない。
これも同様の問題かどうかはわかりませんが、同じエラーが発生することがあります。
この問題が発生したとき、.nextディレクトリを削除して再度実行したyarn devところ、このエラーは発生しなくなりました。
clsxのライブラリが公式で説明されるってことは、一般的によく使われるライブラリってことかな。
【TODO】スタイルについて、styled-jsx
や styled-components
、emotion
については後で、調べよう。
ビルド時にダウンロードしておくことで、ページを表示した時のレイアウトシフトを防ぐ効果があるんだね。
フォントは Web サイトのデザインにおいて重要な役割を果たしますが、プロジェクトでカスタム フォントを使用すると、フォント ファイルをフェッチしてロードする必要がある場合にパフォーマンスに影響を与える可能性があります。
フォントの場合、ブラウザが最初にフォールバック フォントまたはシステム フォントでテキストをレンダリングし、読み込まれた後にカスタム フォントに置き換えるときに、レイアウトのシフトが発生します。この入れ替えにより、テキストのサイズ、間隔、レイアウトが変更され、周囲の要素が移動する可能性があります。
画像の最適化を行うとは、手動でやらなくちゃいけないことを自動で行ってくれるってことか。
ただし、これは手動で以下を行う必要があることを意味します。
・画像がさまざまな画面サイズで応答することを確認します。
・さまざまなデバイスの画像サイズを指定します。
・画像の読み込み時にレイアウトがずれるのを防ぎます。
・ユーザーのビューポートの外にある画像を遅延読み込みします。画像の最適化は Web 開発における大きなトピックであり、それ自体が専門分野であると考えられます。これらの最適化を手動で実装する代わりに、next/imageコンポーネントを使用して画像を自動的に最適化できます。
アスペクト比が同じなら、サイズは如何様でもいい感じかな。
レイアウトのずれを避けるために、画像のwidthとheightを設定することをお勧めします。これらはソース画像と同じアスペクト比にする必要があります。
layout.tsxを作成することで、children
部分だけをレンダリングするから、layout.tsxの部分はレンダリングが1回だけでよくなるということね。
Next.js でレイアウトを使用する利点の 1 つは、ナビゲーション時にページ コンポーネントのみが更新され、レイアウトは再レンダリングされないことです。これは部分レンダリングと呼ばれます。
SPAについて勘違いしていたな。
初回の読み込み時に全てのページをロードするものだと思っていたいけど、実際は適宜フェッチしてデータを取得しているんだね。
SPA (シングルページアプリケーション)
SPA (シングルページ アプリケーション) は、単一の Web ドキュメントのみを読み込み、別のコンテンツを表示するときにFetchなどの JavaScript API を介してその単一ドキュメントの本文コンテンツを更新する Web アプリの実装です。その結果、ユーザーはサーバーから新しいページをすべて読み込むことなくウェブサイトを利用できるようになり、パフォーマンスが向上し、よりダイナミックな体験ができるようになる。ただし、SEOや、状態の維持、ナビゲーションの実装、意味のあるパフォーマンス監視に必要な労力が増えるなどのトレードオフのデメリットもある。
従来はJSを全てロードしていたけど、今はルートごとにロードされるって感じかな。
これって、サーバー側のリソースが分割されているという認識で良いのかな?
ナビゲーション体験を向上させるために、Next.jsはルートセグメントごとにアプリケーションを自動的にコード分割します。これは、ブラウザが初期ロード時にすべてのアプリケーションコードをロードする従来のReactSPAとは異なります。
ルートによってコードが分割されるということは、ページが分離されるということです。特定のページがエラーをスローしても、アプリケーションの残りの部分は動作します。
<Link>
コンポーネントを使用すると、自動的にプリフェッチしてくれる。
任意でプレフェッチをさせたい場合は、useRouter
フックのrouter.prefetch()
を使用すれば、できると。
特に理由がなければ、<Link>
コンポーネントを使用しましょうねという話だね。
Next.jsでルートがプリフェッチされる方法は2つあります:
<Link>コンポーネント: ルートはユーザーのビューポートに表示されると自動的にプリフェッチされます。プリフェッチは、ページが最初にロードされたとき、またはスクロールによって表示されたときに行われます。
router.prefetch(): useRouterフックを使って、プログラムでルートをプリフェッチできます。
【TODO】ナビゲーションについては、割と奥が深そうだな。後で調べようと思う。
usePathname()のフックを使用する際は、'use client';
をファイルの先頭に追加する必要がある。
いや、これはフックを使用するなら、必須なことかな?
↓'use client';
がないと怒られる
Error: usePathname only works in Client Components. Add the "use client" directive at the top of the file to use it.
'use client';
は複雑だな。
分かることは、フック使うならClient Componentだから'use client';
をつけようぜ!
親コンポーネントがClient Componentならいらないけどな!
ってこと。
【TODO】これって、Server ComponentがClient Componentを含んでいる時って、動作的にはサーバー側でレンダリングした後に、ブラウザ側でClient Componentがレンダリングされる感じなのかな?
逆もまた然り?うーん、調べる必要がありそうだな。
どうやら、Server ComponentがClient Componentをインポートすることはできるが、Client ComponentがServer Componentをインポートすることはできないらしい。
もし、Server ComponentをClient Componentが使用したいのであれば、シリアライズ化されたコンポーネントをpropsで受け取って利用してね、ということらしい。
クライアントコンポーネントとサーバーコンポーネントを混在させる場合、UIをコンポーネントのツリーとして視覚化すると便利です。サーバーコンポーネントであるルートレイアウトから始めて、"use client "ディレクティブを追加することで、コンポーネントのサブツリーをクライアントにレンダリングすることができます。
これらのクライアントサブツリー内でも、Server Component をネストしたり、Server Actions を呼び出したりすることができますが、いくつか注意すべき点があります:リクエストとレスポンスのライフサイクルの間に、コードはサーバーからクライアントに移動します。リクエストとレスポンスのライフサイクルの間に、コードはサーバーからクライアントに移動します。クライアントにいる間にサーバーのデータやリソースにアクセスする必要がある場合は、サーバーに新しいリクエストを行います。
サーバーに新しい要求が行われると、クライアントコンポーネント内にネストされたものも含め、すべてのサーバーコンポーネントが最初にレンダリングされます。レンダリング結果(RSCペイロード)には、クライアントコンポーネントの位置への参照が含まれます。次に、クライアントでReactはRSCペイロードを使用して、サーバーコンポーネントとクライアントコンポーネントを単一のツリーに調整します。
クライアント コンポーネントはサーバー コンポーネントの後にレンダリングされるため、サーバー コンポーネントをクライアント コンポーネント モジュールにインポートすることはできません(サーバーに新しいリクエストを返す必要があるため)。その代わりに、クライアントコンポーネントにサーバーコンポーネントをpropsとして渡すことができます。以下のサポートされていないパターンと サポートされているパターンのセクションを参照してください。
【NG】
'use client'
// サーバーコンポーネントをクライアントコンポーネントにインポートすることはできない。
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
【OK】
'use client'
import { useState } from 'react'
export default function ClientComponent({
children, // ← props
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children} // ← サーバ上でレンダリングされた後のコンポーネントを受け取っている。
</>
)
}
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// pageはデフォルトでサーバーコンポーネント。
export default function Page() {
return (
<ClientComponent>
<ServerComponent /> // ← <ClientComponent>よりも前にサーバ上でレンダリングされる。
</ClientComponent>
)
}
server-only
やclient-only
をインストールしていれば、TS/JSファイルをクライアント専用かサーバ専用かと明記できるから、間違ってインポートしてもビルド時にエラーで止めてくれるらしい。
APIキー漏洩とか洒落にならないから、事故を未然に防げるという意味でインストールするのは必須っぽいな。
一見すると、getDataサーバーとクライアントの両方で動作するように見えます。ただし、この関数にはAPI_KEY、サーバー上でのみ実行されることを意図して作成された が含まれています。
この種のクライアントによるサーバー コードの意図しない使用を防ぐために、このserver-onlyパッケージを使用して、他の開発者がこれらのモジュールの 1 つを誤ってクライアント コンポーネントにインポートした場合にビルド時エラーを与えることができます。
設定する必要があるけど、Vercelはプルリクエスト時にプレビューURLを発行してくれる機能があるのか。これは便利。
最近だと当たり前の機能なのかな。
Gitを使ってプルリクエストやマージリクエストを作成した場合、生成されたプレビューURLがVercelボットのコメントとして利用可能になります。このURLは常に最新のデプロイメント変更を反映します。
どうやら、VercelのPostgresSQLのリージョンに日本はないらしい。
Region Code | Region Name | Location |
---|---|---|
cle1 | us-east-2 | Cleveland, USA |
iad1 | us-east-1 | Washington, D.C., USA |
pdx1 | us-west-2 | Portland, USA |
fra1 | eu-central-1 | Frankfurt, Germany |
sin1 | ap-southeast-1 | Singapore |
syd1 | ap-southeast-2 | Sydney, Australia |
Vercel PostgresSQLのバージョンはPostgreSQLバージョン15である、と。
Vercel Postgresの仕組み
ダッシュボードでVercel Postgresデータベースを作成すると、PostgreSQLバージョン15を実行するサーバーレスデータベースが指定したリージョンにプロビジョニングされます。このリージョンは読み取りと書き込みの操作がルーティングされる場所です。
レスポンスタイムを最速にするために、サーバーレスファンクションやエッジファンクションと同じリージョンを選択することをお勧めします。データベースを作成した後、そのリージョンを変更することはできません。データベースを作成する前に、プロジェクトのリージョンを確認してください。
bun run seed
を実行したら、エラーが発生した。また、bcryptか。。。
$ node -r dotenv/config ./scripts/seed.js
error: Cannot find module "/home/bun/app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node" from "/home/bun/app/node_modules/bcrypt/bcrypt.js"
error: script "seed" exited with code 1
手動でコンパイルする必要があるっぽい。
CI環境を構築する際には以下のようにコマンドを記載する必要がある。
cd ./../../node_modules/bcrypt/
bun node-pre-gyp install --fallback-to-build
この問題を抱えている他の人のために、ここに私がそれを解決した方法を示します。何らかの理由で手動でコンパイルする必要がありました。
cd node_modules/bcrypt
node-pre-gyp install --fallback-to-build
手動でコンパイルをする原因がわからなくて、一度エラーが発生する状態に戻したらエラーが解消できなくなった。なんで!?
そもそもBunのUtilsにハッシュパスワードの生成機能があるから、それを使えば良いのか。
なので、bcriptのライブラリを削除して、seed.js
のコードを修正。
const insertedUsers = await Promise.all(
users.map(async (user) => {
- const hashedPassword = await bcrypt.hash(user.password, 10);
+ const hashedPassword = await Bun.password.hash(
+ user.password,
+ {
+ algorithm: 'bcrypt',
+ cost: 10
+ }
+ );
return client.sql`
INSERT INTO users (id, name, email, password)
VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword})
ON CONFLICT (id) DO NOTHING;
`;
}),
);
デフォルトでは、Next.js アプリケーションはReact Server Componentsを使用します。
サーバー コンポーネントはサーバー上で実行されるため、高価なデータのフェッチとロジックをサーバー上に保持し、結果のみをクライアントに送信できます。
前述したように、サーバー コンポーネントはサーバー上で実行されるため、追加の API レイヤーを使用せずにデータベースに直接クエリを実行できます。
Vercel Postgres SDK はSQL インジェクションに対する保護を提供します。
fetchCardData
のように一つの関数にした方が良いのかな。
ついつい汎用的に作ってしまうから、学び。
export async function fetchCardData() {
try {
// You can probably combine these into a single SQL query
// However, we are intentionally splitting them to demonstrate
// how to initialize multiple queries in parallel with JS.
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
const numberOfInvoices = Number(data[0].rows[0].count ?? '0');
const numberOfCustomers = Number(data[1].rows[0].count ?? '0');
const totalPaidInvoices = formatCurrency(data[2].rows[0].paid ?? '0');
const totalPendingInvoices = formatCurrency(data[2].rows[0].pending ?? '0');
return {
numberOfCustomers,
numberOfInvoices,
totalPaidInvoices,
totalPendingInvoices,
};
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch card data.');
}
}
ふむふむ、データの変更が起こっても、画面に範囲されないしか、わからん。
ただし...注意しなければならないことが 2 つあります。
1.データ リクエストは意図せずに相互にブロックし、リクエスト ウォーターフォールを作成します。
2.デフォルトでは、Next.js はパフォーマンスを向上させるためにルートを事前レンダリングします。これは静的レンダリングと呼ばれます。したがって、データが変更されても、ダッシュボードには反映されません。
あー、ブロッキングするから、Promise.all()
とか使って、並列処理した方がパフォーマンスいいよってことね。
「ウォーターフォール」とは、前のリクエストの完了に依存する一連のネットワーク リクエストを指します。データフェッチの場合、各リクエストは、前のリクエストがデータを返した後にのみ開始できます。
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finish
Promise.all()
の場合、一つでのreject
されると、エラーハンドリング行き。
Promise.allSettled()
の場合、一つでのreject
があっても通常のハンドリング行き。
どれかエラーが発生しても、後続の処理を続けたいならPromise.allSettled()
を使うって感じかな。
その場合、各Promiseのステータスをチェックして、場合分けをする処理を機作する必要がある
ドキュメントを見る感じ、ORMと組み合わせるな、KyselyかDrizzleかな。
Prismaは移行システムやデータベース管理インターフェイスを含んでいるけど、そこまでいらない気がする。
マイグレーションをする際に.sql
ファイルを作成して、インフラチームに連携するなら、Drizzle。
プログラマーがマイグレーション作業も行うなら、Kysely。
って感じかな。
ただ、Drizzleの場合、前のバージョンに戻すSQLは自分で書かないといけない。
けど、そもそもマイグレーションを行う直前にバックアップは取るはずだから、いらない気もする。
Vercel Postgres は、多くの一般的な ORM とともに使用できます。一般に、Postgres データベースにアクセスするには、推奨される ORM のいずれか、または好みのクライアントを使用することをお勧めします。
Vercel Postgres はSDKを提供しますが、大規模なアプリケーションにはORMを使用することをお勧めします。
Kysely
Prisma
Drizzle
そういえば、Bun環境で開発しているけど、Vercelで特に設定してないなと思い調べたら、Bunをサポートするようになっていた。bun install
を設定する必要もないし、bun run build
も設定する必要がない。確かにビルドログを見ると、自動で実行されていた。
[18:38:12.634] Installing dependencies...
[18:38:12.687] bun install v1.0.32 (5fec71bd)
[18:38:17.094] Saved lockfile
[18:38:17.102]
[18:38:17.102] + @tutorial-next14/nextjs-dashboard@workspace:packages/nextjs-dashboard
[18:38:17.102]
[18:38:17.102] 523 packages installed [4.43s]
[18:38:17.118] Detected Next.js version: 14.1.1
[18:38:17.121] Running "bun run build"
[18:38:17.127] $ next build
[18:38:17.750] Attention: Next.js now collects completely anonymous telemetry regarding usage.
[18:38:17.751] This information is used to shape Next.js' roadmap and prioritize features.
[18:38:17.751] You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
[18:38:17.751] https://nextjs.org/telemetry
[18:38:17.751]
[18:38:17.847] ▲ Next.js 14.1.1
[18:38:17.848]
[18:38:17.918] Creating an optimized production build ...
[18:38:32.193] ✓ Compiled successfully
静的レンダリングは、静的なブログ投稿や製品ページなど、ユーザーに合わせてカスタマイズされておらず、構築時に認識できるデータがルートに含まれている場合に役立ちます。
動的レンダリングは、ルートにユーザーに合わせてカスタマイズされたデータが含まれる場合、または Cookie や URL の検索パラメーターなど、リクエスト時にのみ知り得る情報が含まれる場合に役立ちます。
オンデマンド再検証をする場合は手動で、サーバーアクションやルートハンドラー内でrevalidatePath()
かrevalidateTag()
を呼び出す必要があると。これめんどいな。サーバーアクションやルートハンドラーのラップ関数を作成して、パラメータで設定する感じが良いかもしれない。
データの再検証
再検証は、データ キャッシュを消去し、最新のデータを再フェッチするプロセスです。これは、データが変更され、最新の情報を確実に表示したい場合に便利です。キャッシュされたデータは、次の 2 つの方法で再検証できます。
時間ベースの再検証: 一定の時間が経過した後にデータを自動的に再検証します。これは、頻繁に変更されず、鮮度がそれほど重要ではないデータに役立ちます。
オンデマンド再検証: イベント (フォーム送信など) に基づいてデータを手動で再検証します。オンデマンド再検証では、タグベースまたはパスベースのアプローチを使用して、データのグループを一度に再検証できます。これは、最新のデータをできるだけ早く表示したい場合 (ヘッドレス CMS のコンテンツが更新された場合など) に便利です。
CDNサーバーの設定はどうやるんだろう?
静的レンダリング (デフォルト)
静的レンダリングを使用すると、ルートはビルド時、またはデータの再検証後にバックグラウンドでレンダリングされます。結果はキャッシュされ、コンテンツ配信ネットワーク (CDN)にプッシュできます。。この最適化により、レンダリング作業の結果をユーザー間およびサーバー リクエスト間で共有できるようになります。
ここのキャッシュってどこにキャッシュされるんだ?CDNにキャッシュって意味?
ユーザーがアプリケーションにアクセスするたびに、キャッシュされた結果が提供されます。静的レンダリングにはいくつかの利点があります。
・Web サイトの高速化- 事前にレンダリングされたコンテンツをキャッシュし、グローバルに配布できます。これにより、世界中のユーザーがより迅速かつ確実に Web サイトのコンテンツにアクセスできるようになります。
・サーバー負荷の軽減- コンテンツがキャッシュされるため、サーバーはユーザーのリクエストごとにコンテンツを動的に生成する必要がありません。
・SEO - 事前にレンダリングされたコンテンツは、ページの読み込み時にすでに利用可能であるため、検索エンジンのクローラーにとってインデックス付けが容易です。これにより、検索エンジンのランキングが向上する可能性があります。
動的レンダリングでは、リクエスト時(ユーザーがページにアクセスしたとき) に、各ユーザーのコンテンツがサーバー上でレンダリングされます。動的レンダリングにはいくつかの利点があります。
・リアルタイム データ- 動的レンダリングにより、アプリケーションはリアルタイムまたは頻繁に更新されるデータを表示できます。これは、データが頻繁に変更されるアプリケーションに最適です。
・ユーザー固有のコンテンツ- ダッシュボードやユーザー プロファイルなどのパーソナライズされたコンテンツを提供し、ユーザー インタラクションに基づいてデータを更新することが簡単になります。
・リクエスト時の情報- 動的レンダリングを使用すると、Cookie や URL 検索パラメータなど、リクエスト時にのみ知ることができる情報にアクセスできます。
省略とは?
→これ翻訳ミスだ。「opt out」は「取りやめる/免れる」っていう意味があるから、この場合は、キャッシュをしないようにする」つまり、常に最新のデータを取得するようになるっていうことかな?
サーバーコンポーネントやデータ取得関数の内部でunstable_noStoreと呼ばれるNext.js APIを使用すると、静的レンダリングを省略できます。
不安定なのでunstable_noStore()
を使わずに、export const dynamic = "force-dynamic"
を記載した方が良い気がするが、おそらくunstable_noStore()
は関数毎に制御できるというメリットがあるのだろう。
注: unstable_noStoreは実験的な API であり、将来変更される可能性があります。独自のプロジェクトで安定した API を使用したい場合は、セグメント構成オプション export const dynamic = "force-dynamic"を使用することもできます。
'force-dynamic':動的レンダリングを強制します。これにより、リクエスト時に各ユーザーに対してルートがレンダリングされます。このオプションは以下と同等です。
・getServerSideProps()ディレクトリ内にありますpages。
・fetch()レイアウトまたはページ内のすべてのリクエストのオプションを に設定します{ cache: 'no-store', next: { revalidate: 0 } }。
・セグメント構成を次のように設定します。export const fetchCache = 'force-no-store'
動的レンダリングを使用すると、アプリケーションは最も遅いデータフェッチと同じ速度でしか動作しません。
データフェッチが重い場合に、ストリーミングを使う、と。
ストリーミングは、ルートをより小さな「チャンク」に分割し、準備が整ったらサーバーからクライアントに段階的にストリーミングできるデータ転送技術です。
ストリーミングすることで、遅いデータ要求によってページ全体がブロックされるのを防ぐことができます。これにより、ユーザーは、UI がユーザーに表示される前にすべてのデータが読み込まれるのを待たずに、ページの一部を表示して操作できるようになります。
「Suspense 上に構築された」ってどういうことだ?
loading.tsxは Suspense 上に構築された特別な Next.js ファイルで、ページ コンテンツの読み込み中に代替として表示するフォールバック UI を作成できます。
loading.tsxに埋め込むUIは、静的ファイルの一部として埋め込まれ、最初に送信されます。その後、残りの動的コンテンツがサーバーからクライアントにストリーミングされます
これ、loading.tsx
でページ全体をサスペンドするよりも、<Suspense>
コンポーネントを使って、コンポーネントで管理した方がいいんだろうな。
React Suspense を使用すると、より詳細に特定のコンポーネントをストリーミングできます。
複数のコンポーネントをSuspenseするとき、ラッパーコンポーネントを作成する理由がいまいちわからないな。下記みたいにしても同じなのでは?と思ってしまう。
<Suspense fallback={<CardsSkeleton />}>
// ↓じゃ、だめなの?<CardWrapper />をわざわざ作成する理由がわからない
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</Suspense>
UXの問題か。特に問題がないのなら、下記の通りにコンポーネントで管理するべきってことかな。
export default function SampleComponent() {
return (
<Suspence fallback={<SampleComponentSkelton />}>
<div>fetched data</div>
</Suspence>
)
}
export default function Page() {
return (
<SampleComponent />
)
}
サスペンスの境界をどこに置くかは、アプリケーションによって異なります。一般的には、データの取得を必要なコンポーネントに移し、そのコンポーネントをサスペンスでラップするのが良い方法です。しかし、あなたのアプリケーションに必要であれば、セクションやページ全体をストリーミングしても問題はありません。
Susppenseは、より楽しいユーザー体験を生み出すための強力なAPIです。
next.config.js
ファイルのprefixAsset
にドメインを設定すれば、良いらしい。
なお、Vercelの場合は自動で設定されるため、手動でnext.config.js
ファイルに記述する必要はないのか。
CDNサーバーの設定はどうやるんだろう?
注意: Vercel にデプロイすると、 Next.js プロジェクトのグローバル CDN が自動的に構成されます。アセット プレフィックスを手動で設定する必要はありません。
今のままだと、サイドバーは静的でデータ取得するコンポーネントは動的。
でも、コンポーネントが動的のため、サイドバー含めてルート全体(サンプルだと、http://localhost:3000/dashboard
のページ全体)が動的になってしまうのか。
で、これだとパフォーマンス悪いから、部分的なプレレンダリングが必要、ってことかな。
現在、ルートの内部で動的な関数 (たとえば noStore() や cookies() など) を呼び出すと、ルート全体が動的になります。
これが今日のほとんどのWebアプリケーションの構築方法です。アプリケーション全体、あるいは特定のルートに対して、静的レンダリングと動的レンダリングのどちらかを選択します。
いや、Suspence
を活用すれば、部分的なプレレンダリングになるのか。
部分的なプレレンダリングはReactのConcurrent APIを活用し、Susppenseを使用して、何らかの条件が満たされるまで(例えばデータがロードされるまで)アプリケーションの一部のレンダリングを延期します。
要約すると、アプリケーションでのデータ取得を最適化するために次のことを行いました。
1.サーバーとデータベース間の待ち時間を短縮するために、アプリケーション コードと同じリージョンにデータベースを作成しました。
2.React Server Components を使用してサーバー上のデータを取得しました。これにより、高価なデータのフェッチとロジックをサーバー上に保持し、クライアント側の JavaScript バンドルを削減し、データベースの秘密がクライアントに公開されるのを防ぐことができます。
3.SQL を使用して必要なデータのみを取得することで、リクエストごとに転送されるデータ量と、メモリ内のデータを変換するために必要な JavaScript の量を削減しました。
4.JavaScript を使用してデータのフェッチを並列化します。そうすることが合理的である場合。
5.遅いデータ要求によってページ全体がブロックされるのを防ぎ、ユーザーがすべてが読み込まれるのを待たずに UI の操作を開始できるようにするために、ストリーミングを実装しました。
6.データのフェッチを必要なコンポーネントに移動し、部分的な事前レンダリングに備えてルートのどの部分を動的にする必要があるかを分離します。
サンプルの通りだと、useSearchParams
で得られる型はReadonlyURLSearchParams
であり、
URLSearchParams
のコンスタラクタの引数にはReadonlyURLSearchParams
を受け取れるようになっていないため、エラーになる。
同様の問題が発生するというIssueを発見。そこではArray.from(searchParams.entries())
として、
一旦配列に変換している。が、公式のサンプルのようにsearchParams.toString()
にしても動作するので、公式にならってtoString()
で文字列に変換するようにする。
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
}
// ...
}
const params = new URLSearchParams(Array.from(searchParams.entries()));
export default function ExampleClientComponent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set(name, value)
return params.toString()
},
[searchParams]
)
return (
<>
<p>Sort By</p>
{/* using useRouter */}
<button
onClick={() => {
// <pathname>?sort=asc
router.push(pathname + '?' + createQueryString('sort', 'asc'))
}}
>
ASC
</button>
{/* using <Link> */}
<Link
href={
// <pathname>?sort=desc
pathname + '?' + createQueryString('sort', 'desc')
}
>
DESC
</Link>
</>
)
}
Next.js側でsearchParams
というURLのクエリを取得できるオプションが用意されている。
これで簡単に、クエリオブジェクトを取得することができる。
useSearchParams()フックとsearchParamsプロップの使い分けは?
検索パラメータを抽出するために2つの異なる方法を使用していることに気づいたかもしれません。どちらを使うかは、クライアントで作業しているかサーバで作業しているかによります。
<Search>はクライアント・コンポーネントなので、クライアントからパラメータにアクセスするためにuseSearchParams()フックを使用しました。
<Table>はそれ自身のデータを取得するServer Componentなので、ページからコンポーネントにsearchParams propを渡すことができます。一般的なルールとして、クライアントからパラメータを読み込みたい場合、useSearchParams()フックを使用します。
「デバウンス」か、初めて聞く。メモメモ。
デバウンスは、関数が起動できる速度を制限するプログラミング手法です。この例では、ユーザーが入力をやめたときにのみデータベースにクエリを実行する必要があります。
デバウンスの仕組み:
トリガーイベント: デバウンスする必要があるイベント (検索ボックスのキーストロークなど) が発生すると、タイマーが開始します。
待機: タイマーが期限切れになる前に新しいイベントが発生すると、タイマーはリセットされます。
実行: タイマーがカウントダウンの終わりに達すると、デバウンス関数が実行されます。
use-debounce
を使うのが一般的なのかな。
サーバー コンポーネント内でサーバー アクションを呼び出すことの利点は、段階的な機能拡張です。クライアントで JavaScript が無効になっている場合でもフォームは機能します。
【TODO】キャッシュ周りはあとで、調べよう
サーバーアクションにはuse server;
を付ける。
'use server'を追加することで、ファイル内にエクスポートされたすべての関数をサーバー関数としてマークすることができます。これらのサーバー関数は、クライアントコンポーネントやサーバーコンポーネントにインポートすることができます。
なるほどね。自動でAPIエンドポイントが作成されるのか。
サーバー アクションは舞台裏でPOST APIのエンドポイントを作成します。これが、サーバー アクションを使用するときに API エンドポイントを手動で作成する必要がない理由です。
サーバーアクションのactions.ts
ファイルを追加したあと、開発サーバーを再起動する必要があるっぽい。
これ、bunだけかな。
バリデーションはzodを使う。
細いけど、金額をセントに変換する処理や日付をDate型に変換する処理はUtilsに収めるべきだと感じた。
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
以下のコードを記述すると、Application error: a client-side exception has occurred (see the browser console for more information).
というエラーが発生した。どうやら、try-catch
やPromise
を戻り値にする関数内では使用できないらしい。
// Use Zod to update the expected types
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
// ...
export async function updateInvoice(id: string, formData: FormData) {
const { customerId, amount, status } = UpdateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
redirect関数でリダイレクトできない原因についてですが、Next.jsのredirect関数の内部で例外(エラー)を発生させる実装になっていることが原因のようです。
サーバーアクションはPromise
じゃないとダメらしい。
これ、サーバーアクション内でredirect
できないじゃん。
似たようなIssueがあるな。解決策がわからないから、サーバーアクションでリダイレクトしないで、クライアント側でリダイレクトするように修正しよう。
まだ機能していない。私は2つのルートレイアウトを持っています。次のバージョンは14.1.0です。今のところ回避策として、サーバーアクションから{status: error | success}のようなステータスを返し、それに基づいてnext/navigationからuseRouterを介してクライアント側でリダイレクトを処理しています。
結構ゴリ押し感があるけど、以下でクライアント側でリダイレクトするようにした。
updateInvoice
内にrevalidatePath
があるのがまだ気になるけど、先ほどのエラーは解消される。
submitUpdateInvoice
関数を作成して、その中でupdateInvoice
を呼びだし、その結果でrevalidatePath
を呼び出すようにした方が良いだろう。または、updateInvoice
の名前を変えるべき。
これだと、汎用的なくせに、/dashboard/invoices
のキャッシュしか無効にしないからね。
(今は面倒なのでやらない)
あと、useFormState
フックで渡すアクションの引数にprev
をつけないとエラーになるのだが、
これいつ使う想定なんだろう?
export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm;
customers: CustomerField[];
}) {
const [state, formAction] = useFormState(
(prev: any, formData: FormData) => updateInvoice(invoice.id, formData),
{ status: '' }
)
useEffect(() => {
if (state.status === 'Success') {
redirect('/dashboard/invoices');
}
}, [state])
return (
<form action={formAction}>
// ...
</form>
)
export async function updateInvoice(id: string, formData: FormData): Promise<ActionResult> {
try {
const { customerId, amount, status } = UpdateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
revalidatePath('/dashboard/invoices');
return {
status: 'Success'
}
} catch (e) {
console.error(e)
return {
status: 'Error'
}
}
}
notFound()
を組み込んだら、エラーになった。なんで!?
error: NEXT_NOT_FOUND
at notFound (:23:19)
at processTicksAndRejections (:61:77)
サーバーアクション内に組み込んでいた。これはクライアントコンポーネントに組み込むのね。
こういうサイトがあるのか。UXの勉強になりそう。
aria-describedby
を使うと、挙動に何か変換かがあるのだろうか?
aria-describedby
がある場合とない場合での違いがよくわからない。
aria-describedby はグローバル属性で、その属性が設定されている要素を説明する要素(複数可)を特定します。
aria-describedby 属性は、オブジェクトを説明する要素の id を列挙します。これは、ウィジェットやグループとそれらを説明するテキストとの関係を確立するために使用します。
aria-describedby 属性はあるフォームコントロールに限ったものではありません。ウィジェットや要素のグループ、見出しのある領域、定義などに静的テキストを関連付けるためにも使用することができます。 aria-describedby 属性は、意味づけされた HTML 要素や ARIA の role を持つ要素で使用することができます。
スクリーンリーダーをオンにした時に、読み上げてくれるようになる。
試しに、Cmd+Option+F5(Mac)を押して、VoiceOverをオンにして読み上げさせたら、aria-describedby
がついているところはエラーメッセージを読み上げてくれた。
だから、aria-describedby
は視覚障害者とか、見えにくい人への配慮だね。
スクリーンリーダーは、画面に表示された内容を視覚以外の方法で伝えるソフトウェアアプリケーションのことです。
まだ、NextAuthの正式版ではNext14に対応していないのか。
業務で使用する際は、要注意だな。
npm install next-auth@beta
satisfies
なんだこれ?って思って調べたら、これは革命すぎる。
今までのイライラが解消される。
type SampleType = {
a: string,
b: string,
b: string | number
}
// OK
// 変数「satisfied」の型は、{ a: string, b: string, c: number }になる
const satisfied = {
a: 'aaa',
b: 'bbb',
c: 0
} satisfies SampleType
// NG
const satisfied = {
a: 'aaa',
b: 'bbb',
c: 0,
d: 'ddd' // 余計なプロパティがあるのでNG
} satisfies SampleType
// 変数「notSatisfied」の型は、{ a: string, b: string, c: string | number }になる
// そのため、cのプロパティをnumberで使用したいときに、
// as numberと強制するか、if文で場合分けしないといけない。
const notSatisfied: SampleType = {
a: 'aaa',
b: 'bbb',
c: 0
}
型アノテーションでもプロパティのキーが充足しているかをチェックできますが、satisfies を使うと型推論が保持されるのが特徴です。
型アノテーションの場合は各キーで取得した値が unknown 型になるので、同じようにはいきません。
Record<Color, string | number[]> を型アノテーションしたとて、値は string | number[] 型になるので、型を絞り込まないと配列として扱うことはできません。
new URL('/dashboard', nextUrl)
のnextUrl
は型エラーになる。
NextURLって、Next.jsの型だからね。それはそうなるよ。
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
NextRequest は Web Request API に便利なメソッドを追加して拡張しています。
これ、auth.ts
でプロバイダーを入れているけど、authConfig
内に入れて良いのではないか?
分ける理由があるのかな?
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [Credentials({})],
});
実行すると下記エラーが発生した。調べたら、これから実装する予定なのか。
error: Attempt to export a nullable value for "TextDecoderStream"
at defineProperties (file:1:1)
at addPrimitives (file:1:13882)
at extend (file:1:13846)
at new VM (file:1:13923)
at new EdgeVM (file:1:13846)
at file:1:92
BunにはNext.jsのMiddlewareに必要なTextDecoderStream
が未実装ということなので、
めでたく終了のお知らせ。素直にNode.jsを使いましょう!!
まじかー。。。
メタデータとは何ですか?
Web 開発では、メタデータは Web ページに関する追加の詳細を提供します。メタデータは、ページを訪問するユーザーには表示されません。代わりに、ページの HTML 内 (通常は要素内) に埋め込まれ、舞台裏で動作します<head>。この隠された情報は、Web ページのコンテンツをより深く理解する必要がある検索エンジンやその他のシステムにとって非常に重要です。
【TODO】Open Graphについては後で調べよう。
なお、Next.jsでBun.password
は使用できない模様。
Bunが実用レベルになるには、まだまだ課題があるね。