Open81

Next14のLearn on Bun

kodukakoduka

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"
kodukakoduka

bun.lockbをテキストで表示すると文字化けするから、なぜ?っと調べたら、
どうやら、bun.lockbはバイナリだった。
https://bun.sh/docs/install/lockfile#why-is-it-binary

なぜバイナリなのでしょうか?
一言で言えば、パフォーマンスです。Bun のロックファイルは、保存とロードが信じられないほど速く、通常のロックファイル内にあるものよりもはるかに多くのデータを保存します。

kodukakoduka

ルートで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"を追記。

kodukakoduka

次は以下のエラーが発生した。

bun install v1.0.21 (837cbd60)
  🔒 Saving lockfile... error: failed to save lockfile: EBUSY

ルートのbun.lockbを削除して、再度実行してもダメだった。

kodukakoduka

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
kodukakoduka

次は以下のエラーが発生したけど、再度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]
kodukakoduka

ファイル構造は以下のようだ。
https://nextjs.org/learn/dashboard-app/getting-started#folder-structure

/app: アプリケーションのすべてのルート、コンポーネント、ロジックが含まれています。主にここから作業します。
/app/lib: 再利用可能なユーティリティ関数やデータ取得関数など、アプリケーションで使用される関数が含まれます。
/app/ui: カード、テーブル、フォームなど、アプリケーションのすべての UI コンポーネントが含まれます。時間を節約するために、これらのコンポーネントは事前にスタイル設定されています。
/public: 画像など、アプリケーションのすべての静的アセットが含まれます。
/scripts: 後の章でデータベースにデータを設定するために使用するシード スクリプトが含まれています。
設定ファイルnext.config.js:アプリケーションのルートなどに設定ファイルがあることにも気づきます。これらのファイルのほとんどは、 を使用して新しいプロジェクトを開始するときに作成され、事前設定されますcreate-next-app。このコースではそれらを変更する必要はありません。

将来的に、libとui、scriptsは別パッケージとして切り出したいな。

kodukakoduka

以下のエラーが出た。

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';としたけど、エラーが治らず。

先のエラーは、ビルド時に以下のようなことが原因で発生するらしいが、それとは別に原因がありそう。けど、原因不明。
https://nextjs.org/docs/messages/fast-refresh-reload

高速リフレッシュでは、ファイルを編集するときに完全なリロードを実行する必要がありました。次のような理由が考えられます。
編集しているファイルには、React コンポーネントに加えて他のエクスポートが含まれている場合があります。
React コンポーネントは匿名関数です。
コンポーネント名は PascalCase ではなく CamelCase で表されます (たとえば、textFieldの代わりにTextField)。

調べているうちに、.nextフォルダを削除して、ビルドしなおせば治るとのこと。
早速試したら、治った。結局、原因がわからない。
https://github.com/vercel/next.js/issues/40184

これも同様の問題かどうかはわかりませんが、同じエラーが発生することがあります。
この問題が発生したとき、.nextディレクトリを削除して再度実行したyarn devところ、このエラーは発生しなくなりました。

kodukakoduka

ビルド時にダウンロードしておくことで、ページを表示した時のレイアウトシフトを防ぐ効果があるんだね。

https://nextjs.org/learn/dashboard-app/optimizing-fonts-images#why-optimize-fonts

フォントは Web サイトのデザインにおいて重要な役割を果たしますが、プロジェクトでカスタム フォントを使用すると、フォント ファイルをフェッチしてロードする必要がある場合にパフォーマンスに影響を与える可能性があります。

フォントの場合、ブラウザが最初にフォールバック フォントまたはシステム フォントでテキストをレンダリングし、読み込まれた後にカスタム フォントに置き換えるときに、レイアウトのシフトが発生します。この入れ替えにより、テキストのサイズ、間隔、レイアウトが変更され、周囲の要素が移動する可能性があります。

kodukakoduka

画像の最適化を行うとは、手動でやらなくちゃいけないことを自動で行ってくれるってことか。

https://nextjs.org/learn/dashboard-app/optimizing-fonts-images#why-optimize-images

ただし、これは手動で以下を行う必要があることを意味します。

・画像がさまざまな画面サイズで応答することを確認します。
・さまざまなデバイスの画像サイズを指定します。
・画像の読み込み時にレイアウトがずれるのを防ぎます。
・ユーザーのビューポートの外にある画像を遅延読み込みします。

画像の最適化は Web 開発における大きなトピックであり、それ自体が専門分野であると考えられます。これらの最適化を手動で実装する代わりに、next/imageコンポーネントを使用して画像を自動的に最適化できます。

kodukakoduka

アスペクト比が同じなら、サイズは如何様でもいい感じかな。

レイアウトのずれを避けるために、画像のwidthとheightを設定することをお勧めします。これらはソース画像と同じアスペクト比にする必要があります。

kodukakoduka

layout.tsxを作成することで、children部分だけをレンダリングするから、layout.tsxの部分はレンダリングが1回だけでよくなるということね。

Next.js でレイアウトを使用する利点の 1 つは、ナビゲーション時にページ コンポーネントのみが更新され、レイアウトは再レンダリングされないことです。これは部分レンダリングと呼ばれます。

kodukakoduka

SPAについて勘違いしていたな。
初回の読み込み時に全てのページをロードするものだと思っていたいけど、実際は適宜フェッチしてデータを取得しているんだね。

https://developer.mozilla.org/en-US/docs/Glossary/SPA

SPA (シングルページアプリケーション)
SPA (シングルページ アプリケーション) は、単一の Web ドキュメントのみを読み込み、別のコンテンツを表示するときにFetchなどの JavaScript API を介してその単一ドキュメントの本文コンテンツを更新する Web アプリの実装です。

その結果、ユーザーはサーバーから新しいページをすべて読み込むことなくウェブサイトを利用できるようになり、パフォーマンスが向上し、よりダイナミックな体験ができるようになる。ただし、SEOや、状態の維持、ナビゲーションの実装、意味のあるパフォーマンス監視に必要な労力が増えるなどのトレードオフのデメリットもある。

kodukakoduka

従来はJSを全てロードしていたけど、今はルートごとにロードされるって感じかな。
これって、サーバー側のリソースが分割されているという認識で良いのかな?

https://nextjs.org/learn/dashboard-app/navigating-between-pages#automatic-code-splitting-and-prefetching

ナビゲーション体験を向上させるために、Next.jsはルートセグメントごとにアプリケーションを自動的にコード分割します。これは、ブラウザが初期ロード時にすべてのアプリケーションコードをロードする従来のReactSPAとは異なります。
ルートによってコードが分割されるということは、ページが分離されるということです。特定のページがエラーをスローしても、アプリケーションの残りの部分は動作します。

kodukakoduka

<Link>コンポーネントを使用すると、自動的にプリフェッチしてくれる。
任意でプレフェッチをさせたい場合は、useRouterフックのrouter.prefetch()を使用すれば、できると。
特に理由がなければ、<Link>コンポーネントを使用しましょうねという話だね。

https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching

Next.jsでルートがプリフェッチされる方法は2つあります:
<Link>コンポーネント: ルートはユーザーのビューポートに表示されると自動的にプリフェッチされます。プリフェッチは、ページが最初にロードされたとき、またはスクロールによって表示されたときに行われます。
router.prefetch(): useRouterフックを使って、プログラムでルートをプリフェッチできます。

kodukakoduka

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.
kodukakoduka

'use client';は複雑だな。
分かることは、フック使うならClient Componentだから'use client';をつけようぜ!
親コンポーネントがClient Componentならいらないけどな!
ってこと。

【TODO】これって、Server ComponentがClient Componentを含んでいる時って、動作的にはサーバー側でレンダリングした後に、ブラウザ側でClient Componentがレンダリングされる感じなのかな?
逆もまた然り?うーん、調べる必要がありそうだな。

https://zenn.dev/luvmini511/articles/ec0e874a2cc1f1

kodukakoduka

どうやら、Server ComponentがClient Componentをインポートすることはできるが、Client ComponentがServer Componentをインポートすることはできないらしい。
もし、Server ComponentをClient Componentが使用したいのであれば、シリアライズ化されたコンポーネントをpropsで受け取って利用してね、ということらしい。

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#interleaving-server-and-client-components

クライアントコンポーネントとサーバーコンポーネントを混在させる場合、UIをコンポーネントのツリーとして視覚化すると便利です。サーバーコンポーネントであるルートレイアウトから始めて、"use client "ディレクティブを追加することで、コンポーネントのサブツリーをクライアントにレンダリングすることができます。
これらのクライアントサブツリー内でも、Server Component をネストしたり、Server Actions を呼び出したりすることができますが、いくつか注意すべき点があります:

リクエストとレスポンスのライフサイクルの間に、コードはサーバーからクライアントに移動します。リクエストとレスポンスのライフサイクルの間に、コードはサーバーからクライアントに移動します。クライアントにいる間にサーバーのデータやリソースにアクセスする必要がある場合は、サーバーに新しいリクエストを行います。

サーバーに新しい要求が行われると、クライアントコンポーネント内にネストされたものも含め、すべてのサーバーコンポーネントが最初にレンダリングされます。レンダリング結果(RSCペイロード)には、クライアントコンポーネントの位置への参照が含まれます。次に、クライアントでReactはRSCペイロードを使用して、サーバーコンポーネントとクライアントコンポーネントを単一のツリーに調整します。

クライアント コンポーネントはサーバー コンポーネントの後にレンダリングされるため、サーバー コンポーネントをクライアント コンポーネント モジュールにインポートすることはできません(サーバーに新しいリクエストを返す必要があるため)。その代わりに、クライアントコンポーネントにサーバーコンポーネントをpropsとして渡すことができます。以下のサポートされていないパターンと サポートされているパターンのセクションを参照してください。

【NG】

app/client-component.tsx
'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】

app/client-component.tsx
'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} // ← サーバ上でレンダリングされた後のコンポーネントを受け取っている。
    </>
  )
}
app/page.tsx
import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
// pageはデフォルトでサーバーコンポーネント。
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent /> // ← <ClientComponent>よりも前にサーバ上でレンダリングされる。
    </ClientComponent>
  )
}
kodukakoduka

server-onlyclient-onlyをインストールしていれば、TS/JSファイルをクライアント専用かサーバ専用かと明記できるから、間違ってインポートしてもビルド時にエラーで止めてくれるらしい。
APIキー漏洩とか洒落にならないから、事故を未然に防げるという意味でインストールするのは必須っぽいな。

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment

一見すると、getDataサーバーとクライアントの両方で動作するように見えます。ただし、この関数にはAPI_KEY、サーバー上でのみ実行されることを意図して作成された が含まれています。

この種のクライアントによるサーバー コードの意図しない使用を防ぐために、このserver-onlyパッケージを使用して、他の開発者がこれらのモジュールの 1 つを誤ってクライアント コンポーネントにインポートした場合にビルド時エラーを与えることができます。

kodukakoduka

設定する必要があるけど、Vercelはプルリクエスト時にプレビューURLを発行してくれる機能があるのか。これは便利。
最近だと当たり前の機能なのかな。

https://vercel.com/docs/deployments/preview-deployments#preview-urls

Gitを使ってプルリクエストやマージリクエストを作成した場合、生成されたプレビューURLがVercelボットのコメントとして利用可能になります。このURLは常に最新のデプロイメント変更を反映します。

kodukakoduka

Vercel PostgresSQLのバージョンはPostgreSQLバージョン15である、と。

https://vercel.com/docs/storage/vercel-postgres#how-vercel-postgres-works

Vercel Postgresの仕組み
ダッシュボードでVercel Postgresデータベースを作成すると、PostgreSQLバージョン15を実行するサーバーレスデータベースが指定したリージョンにプロビジョニングされます。このリージョンは読み取りと書き込みの操作がルーティングされる場所です。
レスポンスタイムを最速にするために、サーバーレスファンクションやエッジファンクションと同じリージョンを選択することをお勧めします。データベースを作成した後、そのリージョンを変更することはできません。データベースを作成する前に、プロジェクトのリージョンを確認してください。

kodukakoduka

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
kodukakoduka

手動でコンパイルする必要があるっぽい。
CI環境を構築する際には以下のようにコマンドを記載する必要がある。

cd ./../../node_modules/bcrypt/
bun node-pre-gyp install --fallback-to-build

https://github.com/kelektiv/node.bcrypt.js/issues/800#issuecomment-628342935

この問題を抱えている他の人のために、ここに私がそれを解決した方法を示します。何らかの理由で手動でコンパイルする必要がありました。
cd node_modules/bcrypt
node-pre-gyp install --fallback-to-build

kodukakoduka

手動でコンパイルをする原因がわからなくて、一度エラーが発生する状態に戻したらエラーが解消できなくなった。なんで!?

kodukakoduka

そもそもBunのUtilsにハッシュパスワードの生成機能があるから、それを使えば良いのか。
なので、bcriptのライブラリを削除して、seed.jsのコードを修正。

scripts/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;
  `;
  }),
);

https://bun.sh/guides/util/hash-a-password

kodukakoduka

https://nextjs.org/learn/dashboard-app/fetching-data#using-server-components-to-fetch-data

デフォルトでは、Next.js アプリケーションはReact Server Componentsを使用します。

サーバー コンポーネントはサーバー上で実行されるため、高価なデータのフェッチとロジックをサーバー上に保持し、結果のみをクライアントに送信できます。
前述したように、サーバー コンポーネントはサーバー上で実行されるため、追加の API レイヤーを使用せずにデータベースに直接クエリを実行できます。

kodukakoduka

fetchCardDataのように一つの関数にした方が良いのかな。
ついつい汎用的に作ってしまうから、学び。

lib/data.ts
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.');
  }
}
kodukakoduka

ふむふむ、データの変更が起こっても、画面に範囲されないしか、わからん。

https://nextjs.org/learn/dashboard-app/fetching-data#practice-fetch-data-for-the-card-components

ただし...注意しなければならないことが 2 つあります。

1.データ リクエストは意図せずに相互にブロックし、リクエスト ウォーターフォールを作成します。
2.デフォルトでは、Next.js はパフォーマンスを向上させるためにルートを事前レンダリングします。これは静的レンダリングと呼ばれます。したがって、データが変更されても、ダッシュボードには反映されません。

kodukakoduka

あー、ブロッキングするから、Promise.all()とか使って、並列処理した方がパフォーマンスいいよってことね。

https://nextjs.org/learn/dashboard-app/fetching-data#what-are-request-waterfalls

「ウォーターフォール」とは、前のリクエストの完了に依存する一連のネットワーク リクエストを指します。データフェッチの場合、各リクエストは、前のリクエストがデータを返した後にのみ開始できます。

page.tsx
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
kodukakoduka

Promise.all()の場合、一つでのrejectされると、エラーハンドリング行き。
Promise.allSettled()の場合、一つでのrejectがあっても通常のハンドリング行き。

どれかエラーが発生しても、後続の処理を続けたいならPromise.allSettled()を使うって感じかな。
その場合、各Promiseのステータスをチェックして、場合分けをする処理を機作する必要がある

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

kodukakoduka

ドキュメントを見る感じ、ORMと組み合わせるな、KyselyかDrizzleかな。
Prismaは移行システムやデータベース管理インターフェイスを含んでいるけど、そこまでいらない気がする。

マイグレーションをする際に.sqlファイルを作成して、インフラチームに連携するなら、Drizzle。
プログラマーがマイグレーション作業も行うなら、Kysely。
って感じかな。

ただ、Drizzleの場合、前のバージョンに戻すSQLは自分で書かないといけない。
けど、そもそもマイグレーションを行う直前にバックアップは取るはずだから、いらない気もする。

https://vercel.com/docs/storage/vercel-postgres/sdk#using-an-orm

Vercel Postgres は、多くの一般的な ORM とともに使用できます。一般に、Postgres データベースにアクセスするには、推奨される ORM のいずれか、または好みのクライアントを使用することをお勧めします。

https://vercel.com/docs/storage/vercel-postgres/using-an-orm

Vercel Postgres はSDKを提供しますが、大規模なアプリケーションにはORMを使用することをお勧めします。

Kysely
Prisma
Drizzle

https://kysely.dev/docs/migrations

https://zenn.dev/satonopan/articles/9182a9eda4d574
https://orm.drizzle.team/kit-docs/overview#migration-files

kodukakoduka

そういえば、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

https://vercel.com/changelog/bun-install-is-now-supported-with-zero-configuration

kodukakoduka

https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default

静的レンダリングは、静的なブログ投稿や製品ページなど、ユーザーに合わせてカスタマイズされておらず、構築時に認識できるデータがルートに含まれている場合に役立ちます。

https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering

動的レンダリングは、ルートにユーザーに合わせてカスタマイズされたデータが含まれる場合、または Cookie や URL の検索パラメーターなど、リクエスト時にのみ知り得る情報が含まれる場合に役立ちます。

kodukakoduka

オンデマンド再検証をする場合は手動で、サーバーアクションやルートハンドラー内でrevalidatePath()revalidateTag()を呼び出す必要があると。これめんどいな。サーバーアクションやルートハンドラーのラップ関数を作成して、パラメータで設定する感じが良いかもしれない。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#revalidating-data

データの再検証
再検証は、データ キャッシュを消去し、最新のデータを再フェッチするプロセスです。これは、データが変更され、最新の情報を確実に表示したい場合に便利です。

キャッシュされたデータは、次の 2 つの方法で再検証できます。

時間ベースの再検証: 一定の時間が経過した後にデータを自動的に再検証します。これは、頻繁に変更されず、鮮度がそれほど重要ではないデータに役立ちます。

オンデマンド再検証: イベント (フォーム送信など) に基づいてデータを手動で再検証します。オンデマンド再検証では、タグベースまたはパスベースのアプローチを使用して、データのグループを一度に再検証できます。これは、最新のデータをできるだけ早く表示したい場合 (ヘッドレス CMS のコンテンツが更新された場合など) に便利です。

kodukakoduka

CDNサーバーの設定はどうやるんだろう?

https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default

静的レンダリング (デフォルト)
静的レンダリングを使用すると、ルートはビルド時、またはデータの再検証後にバックグラウンドでレンダリングされます。結果はキャッシュされ、コンテンツ配信ネットワーク (CDN)にプッシュできます。。この最適化により、レンダリング作業の結果をユーザー間およびサーバー リクエスト間で共有できるようになります。

kodukakoduka

ここのキャッシュってどこにキャッシュされるんだ?CDNにキャッシュって意味?

https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering#what-is-static-rendering

ユーザーがアプリケーションにアクセスするたびに、キャッシュされた結果が提供されます。静的レンダリングにはいくつかの利点があります。

・Web サイトの高速化- 事前にレンダリングされたコンテンツをキャッシュし、グローバルに配布できます。これにより、世界中のユーザーがより迅速かつ確実に Web サイトのコンテンツにアクセスできるようになります。
・サーバー負荷の軽減- コンテンツがキャッシュされるため、サーバーはユーザーのリクエストごとにコンテンツを動的に生成する必要がありません。
・SEO - 事前にレンダリングされたコンテンツは、ページの読み込み時にすでに利用可能であるため、検索エンジンのクローラーにとってインデックス付けが容易です。これにより、検索エンジンのランキングが向上する可能性があります。

kodukakoduka

https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering#what-is-dynamic-rendering

動的レンダリングでは、リクエスト時(ユーザーがページにアクセスしたとき) に、各ユーザーのコンテンツがサーバー上でレンダリングされます。動的レンダリングにはいくつかの利点があります。

・リアルタイム データ- 動的レンダリングにより、アプリケーションはリアルタイムまたは頻繁に更新されるデータを表示できます。これは、データが頻繁に変更されるアプリケーションに最適です。
・ユーザー固有のコンテンツ- ダッシュボードやユーザー プロファイルなどのパーソナライズされたコンテンツを提供し、ユーザー インタラクションに基づいてデータを更新することが簡単になります。
・リクエスト時の情報- 動的レンダリングを使用すると、Cookie や URL 検索パラメータなど、リクエスト時にのみ知ることができる情報にアクセスできます。

kodukakoduka

省略とは?
→これ翻訳ミスだ。「opt out」は「取りやめる/免れる」っていう意味があるから、この場合は、キャッシュをしないようにする」つまり、常に最新のデータを取得するようになるっていうことかな?

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

サーバーコンポーネントやデータ取得関数の内部でunstable_noStoreと呼ばれるNext.js APIを使用すると、静的レンダリングを省略できます。

kodukakoduka

不安定なので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'

kodukakoduka

データフェッチが重い場合に、ストリーミングを使う、と。

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

ストリーミングは、ルートをより小さな「チャンク」に分割し、準備が整ったらサーバーからクライアントに段階的にストリーミングできるデータ転送技術です。

ストリーミングすることで、遅いデータ要求によってページ全体がブロックされるのを防ぐことができます。これにより、ユーザーは、UI がユーザーに表示される前にすべてのデータが読み込まれるのを待たずに、ページの一部を表示して操作できるようになります。

kodukakoduka

「Suspense 上に構築された」ってどういうことだ?

loading.tsxは Suspense 上に構築された特別な Next.js ファイルで、ページ コンテンツの読み込み中に代替として表示するフォールバック UI を作成できます。

kodukakoduka

複数のコンポーネントを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>
kodukakoduka

UXの問題か。特に問題がないのなら、下記の通りにコンポーネントで管理するべきってことかな。

SampleComponent.tsx
export default function SampleComponent() {
    return (
        <Suspence fallback={<SampleComponentSkelton />}>
            <div>fetched data</div>
        </Suspence>
    )
}
page.tsx
export default function Page() {
    return (
        <SampleComponent />
    )
}

https://nextjs.org/learn/dashboard-app/streaming#deciding-where-to-place-your-suspense-boundaries

サスペンスの境界をどこに置くかは、アプリケーションによって異なります。一般的には、データの取得を必要なコンポーネントに移し、そのコンポーネントをサスペンスでラップするのが良い方法です。しかし、あなたのアプリケーションに必要であれば、セクションやページ全体をストリーミングしても問題はありません。

Susppenseは、より楽しいユーザー体験を生み出すための強力なAPIです。

kodukakoduka

next.config.jsファイルのprefixAssetにドメインを設定すれば、良いらしい。
なお、Vercelの場合は自動で設定されるため、手動でnext.config.jsファイルに記述する必要はないのか。

https://zenn.dev/link/comments/ec02d95851d5e0

CDNサーバーの設定はどうやるんだろう?

https://nextjs.org/docs/pages/api-reference/next-config-js/assetPrefix

注意: Vercel にデプロイすると、 Next.js プロジェクトのグローバル CDN が自動的に構成されます。アセット プレフィックスを手動で設定する必要はありません。

kodukakoduka

今のままだと、サイドバーは静的でデータ取得するコンポーネントは動的。
でも、コンポーネントが動的のため、サイドバー含めてルート全体(サンプルだと、http://localhost:3000/dashboardのページ全体)が動的になってしまうのか。
で、これだとパフォーマンス悪いから、部分的なプレレンダリングが必要、ってことかな。

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

現在、ルートの内部で動的な関数 (たとえば noStore() や cookies() など) を呼び出すと、ルート全体が動的になります。

これが今日のほとんどのWebアプリケーションの構築方法です。アプリケーション全体、あるいは特定のルートに対して、静的レンダリングと動的レンダリングのどちらかを選択します。

https://nextjs.org/docs/app/building-your-application/routing/route-handlers#dynamic-functions

kodukakoduka

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

要約すると、アプリケーションでのデータ取得を最適化するために次のことを行いました。

1.サーバーとデータベース間の待ち時間を短縮するために、アプリケーション コードと同じリージョンにデータベースを作成しました。
2.React Server Components を使用してサーバー上のデータを取得しました。これにより、高価なデータのフェッチとロジックをサーバー上に保持し、クライアント側の JavaScript バンドルを削減し、データベースの秘密がクライアントに公開されるのを防ぐことができます。
3.SQL を使用して必要なデータのみを取得することで、リクエストごとに転送されるデータ量と、メモリ内のデータを変換するために必要な JavaScript の量を削減しました。
4.JavaScript を使用してデータのフェッチを並列化します。そうすることが合理的である場合。
5.遅いデータ要求によってページ全体がブロックされるのを防ぎ、ユーザーがすべてが読み込まれるのを待たずに UI の操作を開始できるようにするために、ストリーミングを実装しました。
6.データのフェッチを必要なコンポーネントに移動し、部分的な事前レンダリングに備えてルートのどの部分を動的にする必要があるかを分離します。

kodukakoduka

サンプルの通りだと、useSearchParamsで得られる型はReadonlyURLSearchParamsであり、
URLSearchParamsのコンスタラクタの引数にはReadonlyURLSearchParamsを受け取れるようになっていないため、エラーになる。
同様の問題が発生するというIssueを発見。そこではArray.from(searchParams.entries())として、
一旦配列に変換している。が、公式のサンプルのようにsearchParams.toString()にしても動作するので、公式にならってtoString()で文字列に変換するようにする。

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination#2-update-the-url-with-the-search-params

/app/ui/search.tsx(サンプル)
'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);
  }
  // ...
}

https://github.com/vercel/next.js/issues/49774#issuecomment-1616138326

const params = new URLSearchParams(Array.from(searchParams.entries()));

https://nextjs.org/docs/app/api-reference/functions/use-search-params#updating-searchparams

app/example-client-component.tsx(サンプル)
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>
    </>
  )
}
kodukakoduka

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

useSearchParams()フックとsearchParamsプロップの使い分けは?

検索パラメータを抽出するために2つの異なる方法を使用していることに気づいたかもしれません。どちらを使うかは、クライアントで作業しているかサーバで作業しているかによります。

<Search>はクライアント・コンポーネントなので、クライアントからパラメータにアクセスするためにuseSearchParams()フックを使用しました。
<Table>はそれ自身のデータを取得するServer Componentなので、ページからコンポーネントにsearchParams propを渡すことができます。

一般的なルールとして、クライアントからパラメータを読み込みたい場合、useSearchParams()フックを使用します。

kodukakoduka

「デバウンス」か、初めて聞く。メモメモ。

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination#best-practice-debouncing

デバウンスは、関数が起動できる速度を制限するプログラミング手法です。この例では、ユーザーが入力をやめたときにのみデータベースにクエリを実行する必要があります。

デバウンスの仕組み:

トリガーイベント: デバウンスする必要があるイベント (検索ボックスのキーストロークなど) が発生すると、タイマーが開始します。
待機: タイマーが期限切れになる前に新しいイベントが発生すると、タイマーはリセットされます。
実行: タイマーがカウントダウンの終わりに達すると、デバウンス関数が実行されます。

kodukakoduka

サーバーアクションにはuse server;を付ける。

https://nextjs.org/learn/dashboard-app/mutating-data#2-create-a-server-action

'use server'を追加することで、ファイル内にエクスポートされたすべての関数をサーバー関数としてマークすることができます。これらのサーバー関数は、クライアントコンポーネントやサーバーコンポーネントにインポートすることができます。

kodukakoduka

サーバーアクションのactions.tsファイルを追加したあと、開発サーバーを再起動する必要があるっぽい。
これ、bunだけかな。

kodukakoduka

以下のコードを記述すると、Application error: a client-side exception has occurred (see the browser console for more information).というエラーが発生した。どうやら、try-catchPromiseを戻り値にする関数内では使用できないらしい。

/app/lib/actions.ts(サンプル)
// 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');
}

https://bel-itigo.com/nextjs-redirect-not-working/#:~:text=}-,リダイレクトできない原因,-redirect関数で

redirect関数でリダイレクトできない原因についてですが、Next.jsのredirect関数の内部で例外(エラー)を発生させる実装になっていることが原因のようです。

kodukakoduka

サーバーアクションはPromiseじゃないとダメらしい。
これ、サーバーアクション内でredirectできないじゃん。

kodukakoduka

似たようなIssueがあるな。解決策がわからないから、サーバーアクションでリダイレクトしないで、クライアント側でリダイレクトするように修正しよう。

https://github.com/vercel/next.js/issues/58263#issuecomment-1952812875

まだ機能していない。私は2つのルートレイアウトを持っています。次のバージョンは14.1.0です。今のところ回避策として、サーバーアクションから{status: error | success}のようなステータスを返し、それに基づいてnext/navigationからuseRouterを介してクライアント側でリダイレクトを処理しています。

kodukakoduka

結構ゴリ押し感があるけど、以下でクライアント側でリダイレクトするようにした。
updateInvoice内にrevalidatePathがあるのがまだ気になるけど、先ほどのエラーは解消される。
submitUpdateInvoice関数を作成して、その中でupdateInvoiceを呼びだし、その結果でrevalidatePathを呼び出すようにした方が良いだろう。または、updateInvoiceの名前を変えるべき。
これだと、汎用的なくせに、/dashboard/invoicesのキャッシュしか無効にしないからね。
(今は面倒なのでやらない)

あと、useFormStateフックで渡すアクションの引数にprevをつけないとエラーになるのだが、
これいつ使う想定なんだろう?

app/ui/invoices/edit-form.tsx
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>
)
actions.ts
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'
    }
  }
}
kodukakoduka

notFound()を組み込んだら、エラーになった。なんで!?

error: NEXT_NOT_FOUND
      at notFound (:23:19)
      at processTicksAndRejections (:61:77)
kodukakoduka

サーバーアクション内に組み込んでいた。これはクライアントコンポーネントに組み込むのね。

kodukakoduka

aria-describedbyを使うと、挙動に何か変換かがあるのだろうか?
aria-describedbyがある場合とない場合での違いがよくわからない。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-describedby

aria-describedby はグローバル属性で、その属性が設定されている要素を説明する要素(複数可)を特定します。

aria-describedby 属性は、オブジェクトを説明する要素の id を列挙します。これは、ウィジェットやグループとそれらを説明するテキストとの関係を確立するために使用します。

aria-describedby 属性はあるフォームコントロールに限ったものではありません。ウィジェットや要素のグループ、見出しのある領域、定義などに静的テキストを関連付けるためにも使用することができます。 aria-describedby 属性は、意味づけされた HTML 要素や ARIA の role を持つ要素で使用することができます。

kodukakoduka

スクリーンリーダーをオンにした時に、読み上げてくれるようになる。
試しに、Cmd+Option+F5(Mac)を押して、VoiceOverをオンにして読み上げさせたら、aria-describedbyがついているところはエラーメッセージを読み上げてくれた。

だから、aria-describedbyは視覚障害者とか、見えにくい人への配慮だね。

https://developer.mozilla.org/ja/docs/Glossary/Screen_reader

スクリーンリーダーは、画面に表示された内容を視覚以外の方法で伝えるソフトウェアアプリケーションのことです。

kodukakoduka

satisfiesなんだこれ?って思って調べたら、これは革命すぎる。
今までのイライラが解消される。

sample_satisfies.ts
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
}

https://9sako6.com/posts/why-typescript-satisfies-operator

型アノテーションでもプロパティのキーが充足しているかをチェックできますが、satisfies を使うと型推論が保持されるのが特徴です。

型アノテーションの場合は各キーで取得した値が unknown 型になるので、同じようにはいきません。
Record<Color, string | number[]> を型アノテーションしたとて、値は string | number[] 型になるので、型を絞り込まないと配列として扱うことはできません。