Console.log 的使用方式
在Next.js 16版,有一個新的開發技巧。
我們在Home裡面加上一個console.log,以往的經驗來說這是一個伺服器端的事件,所以在瀏覽器的Console中並不會出現這段輸出內容。
1const Home = () => {2 console.log("我會在伺服器端執行還是客戶端執行?");3 return (4 <div className="text-5xl">Welcome to Next.js!</div>5 )6}7<div></div>8export default Home1const Home = () => {2 console.log("我會在伺服器端執行還是客戶端執行?");3 return (4 <div className="text-5xl">Welcome to Next.js!</div>5 )6}7
8export default Home正常來說只會在伺服器端看到。

可以看到在瀏覽器一樣出現了標示Server的Console內容。
不過不用擔心,這只是在開發的時候方便觀察而已。

這個時候有趣的來了,我要來測試在Client端寫console.log會發生什麼事情,我在app底下建立components資料夾並建立一個hello.tsx檔案。
1"use client"2<div></div>3const Hello = () => {4 console.log("Hello 組件在客戶端執行");5 return (6 <div>Hello</div>7 )8}9<div></div>10export default Hello1"use client"2
3const Hello = () => {4 console.log("Hello 組件在客戶端執行");5 return (6 <div>Hello</div>7 )8}9
10export default Hello將hello的components引入到app/page.tsx。
1import Hello from "@/components/hello"2<div></div>3const Home = () => {4 console.log("我會在伺服器端執行還是客戶端執行?");5 return (6 <main>7 <div className="text-5xl">Welcome to Next.js!</div>8 <Hello />9 </main>10 )11}12<div></div>13export default Home1import Hello from "@/components/hello"2
3const Home = () => {4 console.log("我會在伺服器端執行還是客戶端執行?");5 return (6 <main>7 <div className="text-5xl">Welcome to Next.js!</div>8 <Hello />9 </main>10 )11}12
13export default Home這時候到瀏覽器觀察。會發現有兩個一樣的log。

你可能會問為什麼?我用一張圖給你看看。
在Server端會先建立好static shell,之後在Client端渲染。

React
1npm install babel-plugin-react-compiler@latest1npm install babel-plugin-react-compiler@latest在next.config.ts加上reactCompiler: true,
1import type { NextConfig } from "next";2<div></div>3const nextConfig: NextConfig = {4 reactCompiler: true,5 experimental: {6 turbopackFileSystemCacheForDev: true7 }8<div></div>9};10<div></div>11export default nextConfig;1import type { NextConfig } from "next";2
3const nextConfig: NextConfig = {4 reactCompiler: true,5 experimental: {6 turbopackFileSystemCacheForDev: true7 }8
9};10
11export default nextConfig;因為我對React沒有很熟悉,所以我問了一下ChatGpt。
babel-plugin-react-compiler 能做的事
-
自動最佳化 React 重新渲染
-
自動 memo 化 computation
-
減少 performance bug
-
用更少的 hooks 寫法就能達到最佳性能
-
讓 React 程式碼更簡潔、效果更穩定
-
未來會成為 React 的標準性能工具(官方推薦)
Router

你會看到一個layout.tsk,可以想成是一個網站的佈局,像是header跟footer可以先放好位置,這樣每一個頁面就不必再重複渲染,然後{children}是每一個頁面的進入點,這邊舉個例子像這樣:

Page是你的網站首頁的內容,一進去會先吃到app/page.tsx然後傳到layout.tsx裡面的{children}這裡。
以剛剛的例子:
1import Hello from "@/components/hello"2<div></div>3const Home = () => {4 console.log("我會在伺服器端執行還是客戶端執行?");5 return (6 <main>7 <div className="text-5xl">Welcome to Next.js!</div>8 <Hello />9 </main>10 )11}12<div></div>13export default Home1import Hello from "@/components/hello"2
3const Home = () => {4 console.log("我會在伺服器端執行還是客戶端執行?");5 return (6 <main>7 <div className="text-5xl">Welcome to Next.js!</div>8 <Hello />9 </main>10 )11}12
13export default Home觀察一下瀏覽器的 Elements,已經把app/page.tsx的內容傳到{children}並顯示。

補充:Components不能放在app/資料夾內,app是放Page的地方。
新增一個 Route
直接在app/建立一個資料夾,這邊建立一個about資料夾,跟一個page.tsx。
1app/about/page.tsx2const Page = () => {3 return (4 <div>Page</div>5 )6}7<div></div>8export default Page1app/about/page.tsx2const Page = () => {3 return (4 <div>Page</div>5 )6}7
8export default Page只要在你的網頁打/about路由就會看到Page了。這個在Next.js叫做Filebased routing system。
以下是官方文件的Route示意圖:

Nested Routes 巢狀路由
我建立了一個dashboard,並在dashboard在建立兩個資料夾分別是analytics跟users。

1const Analytics = () => {2 return (3 <div>Analytics</div>4 )5}6<div></div>7export default Analytics1const Analytics = () => {2 return (3 <div>Analytics</div>4 )5}6
7export default Analytics1const Users = () => {2 return (3 <div>Users</div>4 )5}6<div></div>7export default Users1const Users = () => {2 return (3 <div>Users</div>4 )5}6
7export default Users進到 http://localhost:3000/dashboard/analytics 就可以看到 Analytics

進到 http://localhost:3000/dashboard/users 就可以看到 Users

那我們可能會有很多User,每個User都要有自己的Dashboard介面。
因此先在users底下建立一個[id]資料夾,他可以根據你傳進來的params,對應到不同Users的頁面資料。

將每個User外面包一個Link,對應到不同的使用者。將參數傳送到users/[id]。
1import Link from "next/link"2<div></div>3const Users = () => {4 return (5 <div>6 <h1>Dashboard Users</h1>7<div></div>8 <ul className="mt-10">9 <li><Link href="/dashboard/users/1">User 1</Link></li>10 <li><Link href="/dashboard/users/2">User 2</Link></li>11 <li><Link href="/dashboard/users/3">User 3</Link></li>12 <li><Link href="/dashboard/users/4">User 4</Link></li>13 </ul>14 </div>15 )16}17<div></div>18export default Users1import Link from "next/link"2
3const Users = () => {4 return (5 <div>6 <h1>Dashboard Users</h1>7
8 <ul className="mt-10">9 <li><Link href="/dashboard/users/1">User 1</Link></li>10 <li><Link href="/dashboard/users/2">User 2</Link></li>11 <li><Link href="/dashboard/users/3">User 3</Link></li>12 <li><Link href="/dashboard/users/4">User 4</Link></li>13 </ul>14 </div>15 )16}17
18export default Users收到了點擊Link之後傳過來的參數,並顯示使用者id。
1const UserDetails = async ({ params } : { params: Promise<{ id: string }> }) => {2 const { id } = await params;3 return (4 <div>5 <h1>User Details for User {id}</h1>6 </div>7 )8}9<div></div>10export default UserDetails1const UserDetails = async ({ params } : { params: Promise<{ id: string }> }) => {2 const { id } = await params;3 return (4 <div>5 <h1>User Details for User {id}</h1>6 </div>7 )8}9
10export default UserDetails這邊我在學的時候第一次看到{params}這種語法,其實是簡化了以下的程式碼:
1const UserDetails = async (props) => {2 const params = props.params;3}1const UserDetails = async (props) => {2 const params = props.params;3}補充:在 JavaScript 中,所有非同步行為(例如 fetch、資料庫、計時器)最後得到的都是 Promise 才會使用到async、await。這裡的 id其實還不需要用到 await 因為傳來的資料是同步的如下:
1{ id: "1" }2or3{ id: "2" }4or5{ id: "3" }6or7{ id: "4" }1{ id: "1" }2or3{ id: "2" }4or5{ id: "3" }6or7{ id: "4" }
Layout 版面設定
請注意!必須使用 layout.tsx 這個檔案名稱,否則 next.js會認不出來。
我在app/dashboard底下新增layout.tsx
1const Layout = ({ children } : { children: React.ReactNode}) => {2 return (3 <div>4 <div>Dashboard Navbar</div>5 {children}6 </div>7 )8}9<div></div>10export default Layout;1const Layout = ({ children } : { children: React.ReactNode}) => {2 return (3 <div>4 <div>Dashboard Navbar</div>5 {children}6 </div>7 )8}9
10export default Layout;{ children } : { children: React.ReactNode} 的意思是:
這個 component 的 props 有一個 children,而它的型別是 React.ReactNode
React.ReactNode = 可以被 React 放在畫面上的東西
完整的說就是從 props 拿出 children,並且保證它是 React 可渲染的內容。
Route Groups
Route Groups 讓你能夠建立資料夾而且不會影響到 URL。
如果你想要建立一個Layout.tsx,但是不想要當成一個路由,可以使用(admin),這種括號的方式包起來。
以我的資料夾來看:
首頁 http://localhost:3000 會吃到 (root) 底下的page.tsx。
about的話也是會忽略(root)資料夾,指向 http://localhost:3000/about
dashboard也是相同的道理,外層(dashboard)不會吃,會吃內層的dashboard資料夾。

我們可以分別在(dashboard)跟(root)設定不同的layout。並且把最外部原來的page.tsx移動到(root)。
但是要注意,使用括號()的資料夾底下,只能在其中一個保留page.tsx,否則會出現以下錯誤。

Error Handling 錯誤處理
當 Server Component 或 route segment 渲染失敗時,Next.js 會顯示你寫在 error.tsx 的 UI。
而不是預設簡陋的錯誤頁。像是我在about的page寫一個Error,並在about底下建立一個error.tsx。
並進入到about頁面觀察。
1'use client' // Error boundaries must be Client Components2
3import { useEffect } from 'react'4
5export default function Error({6 error,7 reset,8}: {9 error: Error & { digest?: string }10 reset: () => void11}) {12 useEffect(() => {13 // Log the error to an error reporting service14 console.error(error)15 }, [error])16
17 return (18 <div>19 <h2>Something went wrong!</h2>20 <button21 onClick={22 // Attempt to recover by trying to re-render the segment23 () => reset()24 }25 >26 Try again27 </button>28 </div>29 )30}1'use client' // Error boundaries must be Client Components2
3import { useEffect } from 'react'4
5export default function Error({6 error,7 reset,8}: {9 error: Error & { digest?: string }10 reset: () => void11}) {12 useEffect(() => {13 // Log the error to an error reporting service14 console.error(error)15 }, [error])16
17 return (18 <div>19 <h2>Something went wrong!</h2>20 <button21 onClick={22 // Attempt to recover by trying to re-render the segment23 () => reset()24 }25 >26 Try again27 </button>28 </div>29 )30}
可以看到在畫面有顯示文字,同時也可以在console中看到錯誤。
Console只在「開發模式(npm run dev)」才會出現
所以return的部分顯示在前端,useEffect的部分是顯示在後端。

預設的狀態只能看到簡陋的一些文字,無法讓使用者了解目前的狀態。
有了error.tsx,可以呈現一些相關的錯誤訊息給使用者,讓使用者知道目前網站的狀態。
不用擔心重要的資訊外露,因為在client端不會顯示重要訊息,我是把它想成美化error頁面。

Loading UI 載入畫面
在Next.js改如何使用Loading的效果呢?
以下是一個抓取api等待時間的loading效果展示。
1const Page = async () => {2 const res = await fetch("https://jsonplaceholder.typicode.com/users/1");3 const user = await res.json();4 return (5 <div>6 <div>{user.name}</div>;7 </div>8 )9}10<div></div>11export default Page1const Page = async () => {2 const res = await fetch("https://jsonplaceholder.typicode.com/users/1");3 const user = await res.json();4 return (5 <div>6 <div>{user.name}</div>;7 </div>8 )9}10
11export default Page
只要在app/底下建立一個loading.tsx,就會有Loading的效果,它的作用範圍是全站(global loading UI)。
只要任何 page.tsx 或 layout.tsx 在 await 資料,
Next.js 就會顯示 /app/loading.tsx 作為「Loading 過渡畫面」。
子路由可以用自己的 loading.tsx 覆蓋上層的。
Data Fetching 資料截取
第一種方式是透過前端渲染畫面。
但是不推薦,會有延遲,而且原始碼很空SEO效果不好。
1"use client";2<div></div>3import { useState,useEffect } from 'react';4<div></div>5function album1() {6 const [albums, setAlbums] = useState([]);7<div></div>8 useEffect(() => {9 const fetchAlbums = async () => {10 try {11 const response = await fetch("https://jsonplaceholder.typicode.com/albums")12 const data = await response.json();13 setAlbums(data);14 } catch (error) {15 console.error("Error fetching albums:", error);16 }17 };18 fetchAlbums();19 }, []);20<div></div>21 return (22 <div className=''>23 {albums.map((album: {id: number; title: string}) => (24 <div key={album.id} className=''>25 <h3>{album.title}</h3>26 <p>Album ID: {album.id}</p>27 </div>28 ))}29 </div>30 )31}32<div></div>33export default album1;1"use client";2
3import { useState,useEffect } from 'react';4
5function album1() {6 const [albums, setAlbums] = useState([]);7
8 useEffect(() => {9 const fetchAlbums = async () => {10 try {11 const response = await fetch("https://jsonplaceholder.typicode.com/albums")12 const data = await response.json();13 setAlbums(data);14 } catch (error) {15 console.error("Error fetching albums:", error);16 }17 };18 fetchAlbums();19 }, []);20
21 return (22 <div className=''>23 {albums.map((album: {id: number; title: string}) => (24 <div key={album.id} className=''>25 <h3>{album.title}</h3>26 <p>Album ID: {album.id}</p>27 </div>28 ))}29 </div>30 )31}32
33export default album1;另一種方式是伺服器先跑完才放到前端,這種方式速度會更快,用更少的程式碼。
本地開發npm run dev的時候會有Server Components HMR快取,代表會有更快的回應速度,減少呼叫api的次數。
1async function AlbumsPage() {2 const response = await fetch("https://jsonplaceholder.typicode.com/albums");3 if (!response.ok) throw new Error("Failed to fetch data");4<div></div>5 const albums = await response.json();6<div></div>7 return (8 <main className="min-h-screen bg-slate-50 py-8">9 <div className="max-w-4xl mx-auto px-4">10 <h1 className="text-2xl font-semibold text-slate-900 mb-6">11 Albums12 </h1>13<div></div>14 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">15 {albums.map((album: { id: number; title: string }) => (16 <div17 key={album.id}18 className="bg-white rounded-lg border border-slate-200 p-4 shadow-sm19 hover:shadow-md hover:-translate-y-0.520 transition-transform transition-shadow duration-150"21 >22 <h3 className="text-base font-semibold text-slate-900 mb-1">23 {album.title}24 </h3>25 <p className="text-xs text-slate-500">26 ID: {album.id}27 </p>28 </div>29 ))}30 </div>31 </div>32 </main>33 );34}35<div></div>36export default AlbumsPage;1async function AlbumsPage() {2 const response = await fetch("https://jsonplaceholder.typicode.com/albums");3 if (!response.ok) throw new Error("Failed to fetch data");4
5 const albums = await response.json();6
7 return (8 <main className="min-h-screen bg-slate-50 py-8">9 <div className="max-w-4xl mx-auto px-4">10 <h1 className="text-2xl font-semibold text-slate-900 mb-6">11 Albums12 </h1>13
14 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">15 {albums.map((album: { id: number; title: string }) => (16 <div17 key={album.id}18 className="bg-white rounded-lg border border-slate-200 p-4 shadow-sm19 hover:shadow-md hover:-translate-y-0.520 transition-transform transition-shadow duration-150"21 >22 <h3 className="text-base font-semibold text-slate-900 mb-1">23 {album.title}24 </h3>25 <p className="text-xs text-slate-500">26 ID: {album.id}27 </p>28 </div>29 ))}30 </div>31 </div>32 </main>33 );34}35
36export default AlbumsPage;回到伺服器端跟客戶端之間的差別,伺服器端的方式有更多好處,像是載入的時間比較快,FCP表現更好。

也因為在server就先載入好,所以在瀏覽器可以查看到所有原始碼SEO相對也會比較好。

參考 JavaScript Mastery 的教學影片 https://www.youtube.com/watch?v=I1V9YWqRIeI&t=1540s
部分資訊可能已經過時