🧰 ของที่ใช้ตรวจในบทนี้ (รันวันที่ 5 กันยายน 2026)
next 16.3.4 # จาก npx next --version
react 19.2.8 # จาก package.json ของโปรเจกต์
react-dom 19.2.8
typescript 5.9.3 # จาก npx tsc --version
node v24.14.0 # จาก node --version
โปรเจกต์ทดสอบสร้างด้วย npx create-next-app@latest --ts --tailwind --eslint --app --no-src-dir
(App Router · Turbopack) โค้ดทุกไฟล์อยู่ใต้ app/t76/ และ components/t76/
ผลลัพธ์ที่โชว์มาจาก 3 ทาง: npx tsc --noEmit · npx next build ·
และ log ของ npx next dev ตอน curl เข้าไปที่หน้าจริง
ส่วนตัวเลข "วาดใหม่กี่รอบ" ในหัวข้อ 6 มาจาก console.log ในเบราว์เซอร์จริง กดปุ่มจริง
1 · ปัญหาก่อน — ส่ง prop ผ่าน 3 ชั้นให้ดูของจริง
สมมติหน้าเว็บมีข้อมูลผู้ใช้อยู่บนสุด แล้วคนที่ต้องใช้จริง ๆ คือป้ายชื่อเล็ก ๆ ที่อยู่ลึกสุด
โครงเป็นแบบนี้:
ไฟล์จริงที่ผมรัน — app/t76/drilling/page.tsx
(เป็น Server Component ล้วน ไม่มี hook เลย ไม่ต้องใส่ 'use client'):
type User = { name: string; role: string };
function UserBadge({ user }: { user: User }) {
return (
<span>
{user.name} ({user.role})
</span>
);
}
function Menu({ user }: { user: User }) { // ไม่ได้ใช้ user เลย
return (
<nav>
<UserBadge user={user} />
</nav>
);
}
function Sidebar({ user }: { user: User }) { // ก็ไม่ได้ใช้ user เลย
return (
<aside>
<Menu user={user} />
</aside>
);
}
export default function DrillingPage() {
const user: User = { name: 'สมชาย', role: 'admin' };
return (
<main>
<h1>prop drilling</h1>
<Sidebar user={user} />
</main>
);
}
อันนี้ทำงานได้จริง ไม่ได้พัง — ผมรัน dev server แล้ว curl เข้าไปดู:
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:376/t76/drilling
200
# ใน log ของ next dev
GET /t76/drilling 200 in 79ms (next.js: 67ms, application-code: 12ms)
ปัญหาไม่ใช่ "มันพัง" — ปัญหาคือ Sidebar กับ Menu
ถูกบังคับให้รู้จัก user ทั้งที่ไม่ได้ใช้
ลองนึกว่าวันหนึ่งเราอยากส่ง theme ลงไปด้วย ต้องไปแก้กี่ไฟล์? — แก้ทั้ง 4 ไฟล์
ทั้งที่คนใช้จริงมีคนเดียว
แล้วถ้าโครงลึก 6 ชั้น มี 12 component ที่แค่ "ส่งผ่าน"? นั่นแหละที่เรียกว่า prop drilling
ข้อเสียที่แท้จริงของโค้ดข้างบนคืออะไร
ตอบตัวเลือกที่ 3 · ตัวเลือกที่ 1 ผิดเพราะการส่ง prop ต่อกันไม่ได้ทำให้ช้าจนวัดได้
มันแค่เขียนแล้วรำคาญ และแก้ทีหลังยาก ·
ตัวเลือกที่ 2 กับ 4 ไม่จริงเลย — โค้ดข้างบนรันผ่าน HTTP 200 และ
npx tsc --noEmit ก็ผ่านสะอาด ไม่มีใครเตือนอะไรทั้งนั้น
ประเด็นคือ compiler ไม่เตือนเรื่องนี้ให้ เราต้องเห็นเอง
2 · useContext แก้ปัญหานี้ยังไง
แนวคิดคือ แทนที่จะส่งค่าลงไปทีละชั้น เราวางค่าไว้ที่ชั้นบน
แล้วให้คนข้างล่างเอื้อมขึ้นมาหยิบเอง
ทำ 3 ขั้น:
| ขั้น | เขียนว่า | ทำอะไร |
|---|---|---|
| 1 · สร้างช่อง | createContext(ค่าเริ่มต้น) |
สร้าง "ช่องทาง" ขึ้นมาหนึ่งช่อง · ค่าที่ใส่ตรงนี้จะถูกใช้ก็ต่อเมื่อไม่มี Provider ครอบอยู่ |
| 2 · ใส่ค่าลงช่อง | <MyContext value={…}> |
ครอบ subtree ที่อยากให้เห็นค่านี้ · ทุกอย่างที่อยู่ข้างในเอื้อมมาหยิบได้หมด |
| 3 · หยิบค่าออกมา | useContext(MyContext) |
เรียกจาก component ไหนก็ได้ที่อยู่ใต้ Provider · ไม่ต้องมี prop ใด ๆ |
จุดที่คนมักเข้าใจผิดตั้งแต่แรก: useContext ไม่ได้มองหา "Provider ที่ไหนก็ได้ในโปรแกรม"
มันไต่ขึ้นไปตามต้นไม้ของ component จากจุดที่เรียก แล้วหยุดที่ Provider ตัวที่ใกล้ที่สุดที่อยู่เหนือมัน
สไลด์หน้า 55 เขียนเรื่องนี้ไว้ตรง ๆ ว่า "Always look for the closest provider above the component that calls"
และเตือนต่อว่า "Does not consider providers in the component that calls" —
แปลว่า ถ้าคุณ return Provider ออกไปจาก component เดียวกับที่เรียก useContext
ตัวมันเองจะไม่เห็นค่านั้น เพราะ Provider ตัวนั้นอยู่ "ใต้" มัน ไม่ได้อยู่ "เหนือ" มัน
createContext เหมือนติดตั้งท่อน้ำเส้นหนึ่งในตึก ·
<Provider value={…}> เหมือนเปิดวาล์วปล่อยน้ำเข้าท่อที่ชั้นหนึ่ง ·
useContext เหมือนก๊อกน้ำในห้อง เปิดก๊อกแล้วน้ำมาเลย
ไม่ต้องให้คนชั้น 2 กับชั้น 3 หิ้วถังส่งต่อกันลงมา
ที่ไม่เหมือนกัน: น้ำในตึกจริงไหลลงมาจากถังบนดาดฟ้าเสมอ ไม่ว่าห้องคุณจะอยู่ชั้นไหน
แต่ context ไม่ใช่ — มันหยิบจากวาล์วตัวที่ใกล้ที่สุดที่อยู่เหนือห้องคุณ
ถ้าชั้น 5 มีวาล์วของตัวเองอีกอัน ห้องชั้น 6 จะได้น้ำจากวาล์วชั้น 5 ไม่ใช่ของชั้น 1
และที่ต่างชัดกว่านั้น: ถ้าไม่มีวาล์วเลยสักตัว ก๊อกน้ำจริงจะแค่ไม่มีน้ำไหลเฉย ๆ
แต่ useContext จะคืนค่าเริ่มต้นที่ใส่ไว้ตอน createContext
ซึ่ง "ดูเหมือนใช้ได้" — และนั่นคือกับดักที่อันตรายที่สุดของบทนี้ (หัวข้อ 5)
3 · เขียน context ของตัวเองแบบที่ใช้จริงในโปรเจกต์
สไลด์หน้า 54 แสดงท่ามาตรฐานไว้แล้ว: รวม context · Provider · และ hook สำหรับหยิบค่า ไว้ในไฟล์เดียว
แล้วให้ที่อื่น import ไปใช้
นี่คือไฟล์จริงที่ผมรัน components/t76/theme.tsx —
สังเกตบรรทัดแรก 'use client' เดี๋ยวหัวข้อ 7 จะพิสูจน์ให้ดูว่าถ้าไม่ใส่จะเจออะไร:
'use client';
import { createContext, useContext, useMemo, useState, type ReactNode } from 'react';
export type Theme = { mode: 'light' | 'dark'; accent: string };
type ThemeBox = { theme: Theme; toggle: () => void };
// ค่าเริ่มต้นเป็น undefined โดยตั้งใจ — เพื่อให้ "ลืมครอบ Provider" กลายเป็น error ที่อ่านออก
const ThemeContext = createContext<ThemeBox | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>({ mode: 'dark', accent: '#c8b89a' });
// ห่อด้วย useMemo — เหตุผลอยู่ในหัวข้อ 6 และมีผลรันจริงยืนยัน
const value = useMemo<ThemeBox>(
() => ({
theme,
toggle: () =>
setTheme((t) => ({ ...t, mode: t.mode === 'dark' ? 'light' : 'dark' })),
}),
[theme],
);
return <ThemeContext value={value}>{children}</ThemeContext>;
}
export function useTheme(): ThemeBox {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme ต้องเรียกใต้ <ThemeProvider> เท่านั้น');
return ctx;
}
คนที่ใช้ค่าไม่ต้องรู้อะไรเลยนอกจากชื่อ hook — components/t76/badge.tsx:
'use client';
import { useTheme } from './theme';
export function UserBadge() {
const { theme, toggle } = useTheme(); // ไม่มี prop สักตัว
return (
<span>
โหมดตอนนี้: {theme.mode} / {theme.accent}{' '}
<button onClick={toggle}>สลับ</button>
</span>
);
}
แล้วชั้นกลาง — app/t76/page.tsx — สะอาดขึ้นทันที
เพราะ Sidebar กับ Menu ไม่ต้องรับ prop อะไรอีกแล้ว:
ก่อน — ส่งต่อทีละชั้น
function Sidebar({ user }: { user: User }) {
return <aside><Menu user={user} /></aside>;
}
function Menu({ user }: { user: User }) {
return <nav><UserBadge user={user} /></nav>;
}
เพิ่มค่าใหม่หนึ่งตัว = แก้ทุกชั้นที่ค่าเดินผ่าน
หลัง — ครอบ Provider ชั้นเดียว
function Sidebar() {
return <aside><Menu /></aside>;
}
function Menu() {
return <nav><UserBadge /></nav>;
}
เพิ่มค่าใหม่ = แก้แค่ไฟล์ context กับคนที่ใช้จริง
import { ThemeProvider } from '@/components/t76/theme';
import { UserBadge } from '@/components/t76/badge';
function Menu() { return <nav><UserBadge /></nav>; }
function Sidebar() { return <aside><Menu /></aside>; }
export default function T76Page() {
return (
<ThemeProvider>
<main>
<h1>useContext</h1>
<Sidebar />
</main>
</ThemeProvider>
);
}
ผลรันจริงของหน้านี้ — ผ่านทั้ง type check และ build:
$ npx tsc --noEmit
# ไม่มี output = ผ่าน (exit code 0)
$ npx next build
▲ Next.js 16.3.4 (Turbopack)
✓ Running next.config.ts took 10ms
Creating an optimized production build ...
✓ Compiled successfully in 3.0s
Running TypeScript ...
Finished TypeScript in 775ms ...
Collecting page data using 9 workers ...
✓ Generating static pages using 9 workers (8/8) in 83ms
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /t76
├ ○ /t76/both-forms
├ ○ /t76/drilling
├ ƒ /t76/no-guard
├ ƒ /t76/no-provider
├ ○ /t76/rerender
└ ƒ /t76/slide-style
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
/t76/both-forms คือหน้าทดลองของหัวข้อ 4 · ส่วนสามหน้าที่ขึ้น ƒ
คือหน้ากับดักของหัวข้อ 5 ที่ผมตั้งใจใส่ export const dynamic = 'force-dynamic' ไว้
ถ้าไม่ใส่ Next.js จะพยายาม prerender หน้าที่ตั้งใจให้ throw ตั้งแต่ตอน build แล้ว
next build จะพังทั้งโปรเจกต์ ทั้งที่หน้าอื่นเขียนถูกหมด
- prop drilling
- อาการที่ต้องส่ง prop ผ่าน component หลายชั้นทั้งที่ชั้นกลางไม่ได้ใช้ · เป็นคำที่ใช้เวลาอธิบายว่า "ทำไมถึงต้องเอา context เข้ามา"
- Provider
- component ที่ครอบ subtree แล้วกำหนดว่าค่าของ context นี้ในบริเวณนั้นคืออะไร · ใน React 19 เขียนได้สองแบบ (หัวข้อ 4)
- consumer
- component ที่เรียก
useContextเพื่อหยิบค่า · คำนี้สำคัญตอนพูดเรื่อง re-render เพราะ "ของที่วาดใหม่เมื่อ context เปลี่ยน" คือ consumer ทุกตัว ไม่ใช่ทุก component - custom hook
- ฟังก์ชันที่ขึ้นต้นด้วย
useและเรียก hook ตัวอื่นข้างใน —useThemeในบทนี้คือตัวอย่าง · บทหน้า (77) ว่าด้วยเรื่องนี้เต็ม ๆ
4 · Provider มีสองหน้าตา และสไลด์ใช้คนละแบบกันเอง
สไลด์หน้า 53 เขียน <ThemeContext value="dark"> (ไม่มี .Provider)
แต่หน้า 54 เขียน <ThemeContext.Provider value={{ ...theme }}>
ไม่ใช่ว่าหน้าใดหน้าหนึ่งพิมพ์ผิด — ทั้งสองแบบใช้ได้จริงใน React 19
แบบสั้นเป็นของใหม่ที่ React 19 เพิ่มเข้ามา ส่วน .Provider เป็นของเดิม
ผมเขียนทั้งสองแบบไว้ในหน้าเดียวกันแล้วรันจริง:
const Ctx = createContext('default');
<Ctx.Provider value="แบบเก่า .Provider">
<Read tag="old" />
</Ctx.Provider>
<Ctx value="แบบใหม่ React 19">
<Read tag="new" />
</Ctx>
ผลที่ได้จาก HTML ที่ server ส่งกลับมาจริง:
$ curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:376/t76/both-forms
HTTP 200
# ดึงเนื้อใน <p> ออกมา
'old: แบบเก่า .Provider'
'new: แบบใหม่ React 19'
ทั้งคู่ทำงาน และใน react 19.2.8 ตัวนี้ผมไม่เห็น warning ขึ้นในคอนโซลเบราว์เซอร์เลยสักอัน
ดังนั้นโค้ดเก่าที่ใช้ .Provider ยังใช้ได้อยู่ ไม่ต้องรีบไล่แก้
แต่ถ้าเขียนใหม่ ผมแนะนำแบบสั้น <Ctx value={…}> เพราะสั้นกว่าและเป็นทิศทางที่ React เดินไป
const Ctx = createContext('A');
function Leaf() { return <p>{useContext(Ctx)}</p>; }
function App() {
return (
<Ctx value="B">
<Ctx value="C">
<Leaf />
</Ctx>
</Ctx>
);
}
หน้าจอจะขึ้นตัวอักษรอะไร — A, B หรือ C?
ได้ C เพราะ useContext หยุดที่ Provider ตัวที่ใกล้ที่สุดที่อยู่เหนือ Leaf
ซึ่งคือตัวที่ใส่ "C" · ตัว "B" อยู่สูงกว่าจึงถูก "บัง" ·
ส่วน "A" เป็นค่าเริ่มต้นตอน createContext จะถูกใช้ก็ต่อเมื่อไม่มี Provider เลย
ประโยชน์จริงของการซ้อน Provider: ทั้งเว็บใช้ธีมสว่าง แต่กล่อง preview อันเดียวอยากให้เป็นธีมมืด —
ก็แค่ครอบ Provider อีกชั้นตรงนั้น ไม่ต้องแก้อะไรข้างนอก
5 · กับดัก 1 — ลืมครอบ Provider (นี่คืออันที่เจอบ่อยที่สุด)
ผมทำหน้า app/t76/no-provider/page.tsx ที่เรียก <UserBadge /> ตรง ๆ
โดยไม่ครอบ <ThemeProvider> แล้ว curl เข้าไป — ได้ HTTP 500
และ log ของ next dev ขึ้นแบบนี้ (ก็อปมาทั้งดุ้น):
⨯ Error: useTheme ต้องเรียกใต้ <ThemeProvider> เท่านั้น
at useTheme (components/t76/theme.tsx:30:19)
at UserBadge (components/t76/badge.tsx:6:37)
28 | export function useTheme(): ThemeBox {
29 | const ctx = useContext(ThemeContext);
> 30 | if (!ctx) throw new Error('useTheme ต้องเรียกใต้ <ThemeProvider> เท่านั้น');
| ^
31 | return ctx;
32 | }
33 | {
digest: '914102247'
}
GET /t76/no-provider 500 in 143ms (next.js: 114ms, application-code: 28ms)
เลข 30 คือบรรทัดใน theme.tsx ที่โชว์ไว้ในหัวข้อ 3 นับทั้งไฟล์รวมคอมเมนต์
— ถ้าคุณลบคอมเมนต์สองบรรทัดนั้นออก เลขจะเลื่อนเป็น 28 แต่ยังชี้ที่บรรทัด throw เหมือนเดิม
เวลาอ่าน error ให้ดูที่บรรทัดที่มีลูกศร > ไม่ใช่จำเลขบรรทัด
error นี้อ่านออกเพราะเราเขียน guard ไว้เอง ทีนี้ลองดูว่าถ้าไม่เขียน guard จะเป็นยังไง — ผมทำอีกไฟล์ที่หยิบค่ามาใช้ตรง ๆ:
export const RawThemeContext = createContext<Theme | undefined>(undefined);
export function RawBadge() {
const { mode } = useContext(RawThemeContext)!; // ! คือบอก TS ว่า "เชื่อฉัน ไม่ undefined หรอก"
return <span>โหมด: {mode}</span>;
}
npx tsc --noEmit ผ่านฉลุย เพราะเราใส่ ! ปิดปาก compiler ไว้ · แล้วพอรันจริง:
⨯ TypeError: Cannot destructure property 'mode' of '{imported module ...
.../vendored/ssr/react.js}.useContext(...)' as it is undefined.
at RawBadge (components/t76/theme-raw.tsx:10:11)
8 |
9 | export function RawBadge() {
> 10 | const { mode } = useContext(RawThemeContext)!;
| ^
11 | return <span>โหมด: {mode}</span>;
12 | }
GET /t76/no-guard 500 in 121ms (next.js: 82ms, application-code: 39ms)
พังเหมือนกัน แต่ข้อความไม่ได้บอกว่า "ลืมครอบ Provider" เลยสักคำ —
ต้องอ่านออกเองว่า useContext(...) คืน undefined แปลว่าอะไร
นี่คือเหตุผลที่ท่ามาตรฐานคือเขียน hook ครอบพร้อม guard เสมอ อย่าให้ที่อื่นเรียก
useContext ตรง ๆ
จุดที่สไลด์หน้า 54 พลาด และผมรันพิสูจน์แล้ว
โค้ดในสไลด์หน้า 54 เขียนไว้แบบนี้ (คัดมาเฉพาะบรรทัดที่เกี่ยวข้อง):
const ThemeContext = createContext("light"); // ค่าเริ่มต้นเป็น string
export const ThemeProvider = () => { // ไม่ได้รับ children
const [theme, setTheme] = useState({ mode: "dark", palette: { primary: [], secondary: [] } });
return (
<ThemeContext.Provider value={{ ...theme }}>
{children} // แต่ใช้ children
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
ผมก็อปโค้ดนี้มาทั้งดุ้นแล้วรัน npx tsc --noEmit ได้ error 2 อัน:
slide54/theme.tsx(14,28): error TS2322: Type '{ mode: string; palette: { primary: never[]; secondary: never[]; }; }'
is not assignable to type 'string'.
slide54/theme.tsx(15,8): error TS2304: Cannot find name 'children'.
⚠️ สไลด์หน้า 54 มีปัญหา 3 อย่าง — สองอันแรก compiler จับได้ อันที่สามไม่มีใครจับให้
1. TS2304: Cannot find name 'children' —
ThemeProvider = () => { ไม่ได้รับ parameter อะไรเลย แล้วไปใช้ {children}
· แก้โดยเขียนเป็น ({ children }: { children: ReactNode }) => {
2. TS2322 — createContext("light") ทำให้ TypeScript สรุปว่าช่องนี้เก็บ
string แต่ Provider ส่ง object ลงไป · แก้โดยประกาศ type ให้ชัด เช่น
createContext<ThemeBox | undefined>(undefined)
3. อันตรายที่สุด: guard if (!context) ในสไลด์ไม่มีวันทำงาน —
เพราะค่าเริ่มต้นคือ "light" ซึ่งเป็น string ที่ไม่ว่าง แปลว่า truthy
ต่อให้ลืมครอบ Provider จริง ๆ !context ก็เป็น false ตลอด ไม่มีการโยน error
· ดูผลรันข้างล่าง
ผมทำหน้า app/t76/slide-style/page.tsx ที่ใช้ createContext('light') แบบสไลด์
แล้วเรียกใช้ โดยไม่ครอบ Provider เลย — เดาก่อนว่าจะได้อะไร:
const ThemeContext = createContext('light');
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
// หน้าเว็บเรียก <SlideBadge /> ที่ใช้ useTheme() โดยไม่มี Provider ครอบเลย
จะได้ HTTP 500 พร้อม error "must be used within ThemeProvider" หรือได้อย่างอื่น?
ได้ HTTP 200 หน้าขึ้นปกติ ไม่มี error สักตัว — ผลรันจริง:
$ curl -s -o /tmp/ss.html -w 'HTTP %{http_code}\n' http://localhost:376/t76/slide-style
HTTP 200
# เนื้อในหน้าเว็บ
theme = [light]
# log ของ next dev — ไม่มีบรรทัด error เลย
GET /t76/slide-style 200 in 96ms (next.js: 72ms, application-code: 24ms)
guard ที่เขียนไว้ไม่ทำงาน เพราะ "light" เป็น truthy
· component ได้ค่า "light" ไปใช้แล้วดูเหมือนทุกอย่างปกติ
อันนี้แย่กว่าพัง — พังเราแก้ได้ทันที แต่ "ดูเหมือนถูก" จะไปโผล่เป็นบั๊กตอน production
ว่าทำไมธีมไม่เปลี่ยนตาม
ทางแก้: ตั้งค่าเริ่มต้นเป็น undefined แล้วบอก type ให้ชัด
createContext<ThemeBox | undefined>(undefined) — undefined เป็น falsy
guard ถึงจะทำงาน (พิสูจน์แล้วในผลรันบนสุดของหัวข้อนี้)
ทำไม createContext(undefined) ถึงปลอดภัยกว่า createContext("light") ในทางปฏิบัติ
ตอบตัวเลือกที่ 2 · ประเด็นทั้งหมดคือ เปลี่ยนความผิดพลาดที่เงียบ ให้กลายเป็นความผิดพลาดที่ส่งเสียง
· เราพิสูจน์ทั้งสองฝั่งด้วยผลรันจริงแล้ว: ค่าเริ่มต้น "light" ได้ HTTP 200 กับข้อความ
theme = [light] ส่วนค่าเริ่มต้น undefined + guard ได้ HTTP 500 พร้อมชื่อไฟล์และเลขบรรทัด
ตัวเลือกที่ 1 ไม่จริง (React ยังไต่หา Provider เหมือนเดิม) · ตัวเลือกที่ 3 ไม่จริง
(tsc ไม่เคยบังคับเรื่อง useMemo) · ตัวเลือกที่ 4 ไม่จริง
Provider ส่ง object ได้อยู่แล้วถ้าประกาศ type ให้ตรง
6 · กับดัก 2 — value เป็น object literal ทำให้ลูกทุกตัววาดใหม่
บทที่ 75 พูดเรื่อง "ค่าเดิมแต่กล่องใหม่" ไปแล้ว กับดักนี้คือเรื่องเดียวกันเป๊ะ แค่ย้ายมาอยู่บน Provider
ทุกครั้งที่ component แม่วาดใหม่ บรรทัด value={{ n }} จะสร้าง object ก้อนใหม่
· React เทียบค่า context ด้วย Object.is ก้อนใหม่จึง "ไม่เท่า" ก้อนเดิมเสมอ
ทั้งที่ข้างในเหมือนกันทุกตัวอักษร → consumer ทุกตัววาดใหม่หมด
ผมทำหน้าทดลอง app/t76/rerender/page.tsx ที่มี context สองช่องเทียบกัน
ลูกทั้งสองตัวห่อด้วย memo เหมือนกัน และค่า n ไม่เคยเปลี่ยนเลย (เป็น 1 ตลอด)
· ปุ่มที่กดแค่เปลี่ยน tick ซึ่งไม่เกี่ยวกับ context เลย:
const LiteralCtx = createContext<Box>({ n: 0 });
const MemoCtx = createContext<Box>({ n: 0 });
const LiteralChild = memo(function LiteralChild() {
const v = useContext(LiteralCtx);
literalRenders += 1;
console.log('[t76] LiteralChild render #' + literalRenders + ' n=' + v.n);
return <p>literal: {v.n}</p>;
});
// MemoChild เขียนเหมือนกันทุกอย่าง ต่างแค่อ่านจาก MemoCtx
export default function RerenderPage() {
const [tick, setTick] = useState(0);
const n = 1; // ค่าไม่เคยเปลี่ยนเลย
const memoValue = useMemo<Box>(() => ({ n }), [n]);
return (
<main>
<button onClick={() => setTick((t) => t + 1)}>tick = {tick}</button>
<LiteralCtx value={{ n }}> {/* object ก้อนใหม่ทุกรอบ */}
<LiteralChild />
</LiteralCtx>
<MemoCtx value={memoValue}> {/* ก้อนเดิมตราบใดที่ n ไม่เปลี่ยน */}
<MemoChild />
</MemoCtx>
</main>
);
}
กดปุ่ม 2 ครั้ง (tick 0 → 1 → 2) ทั้งที่ n ไม่เปลี่ยนเลย —
LiteralChild กับ MemoChild ตัวไหนจะวาดใหม่บ้าง?
LiteralChild วาดใหม่ทุกครั้ง · MemoChild ไม่วาดใหม่เลยสักครั้ง
นี่คือคอนโซลจริงจากเบราว์เซอร์หลังกดปุ่มสองที (ก็อปมาทั้งดุ้น):
[t76] Parent render, tick=1
[t76] Parent render, tick=1
[t76] LiteralChild render #3 n=1
[t76] LiteralChild render #4 n=1
[t76] Parent render, tick=2
[t76] Parent render, tick=2
[t76] LiteralChild render #5 n=1
[t76] LiteralChild render #6 n=1
ไม่มีบรรทัดของ MemoChild โผล่มาเลย — มันค้างอยู่ที่ render #2 ตั้งแต่ตอนโหลดหน้า
สังเกตว่าทุกบรรทัดขึ้นซ้ำสองรอบ — อันนั้นคือ Strict Mode ของ React ตอน dev
ที่ตั้งใจเรียก component สองรอบเพื่อจับ side effect ไม่ใช่บั๊ก และไม่เกิดตอน production
สังเกตอีกอย่าง: n=1 เท่าเดิมทุกบรรทัด — ค่าไม่เคยเปลี่ยน แต่ก็ยังวาดใหม่
เพราะสิ่งที่เปลี่ยนคือตัวกล่อง ไม่ใช่ค่าในกล่อง
memo เหมือนยามหน้าตึกที่จำหน้าคนได้ — ถ้ามาคนเดิม ยามไม่ต้องเช็คบัตรใหม่
แต่ React ไม่ได้จำ "หน้าคน" มันจำเลขบัตรประชาชนของกล่องที่ถือมา
· value={{ n: 1 }} คือการเอาของเดิมใส่กล่องใหม่ที่มีเลขใหม่ทุกครั้ง
ยามเลยต้องเช็คใหม่ทุกรอบ ทั้งที่ของข้างในเหมือนเดิมเป๊ะ
· useMemo คือการใช้กล่องใบเดิมซ้ำ ตราบใดที่ของข้างในยังเหมือนเดิม
ที่ไม่เหมือนกัน: ยามจริงเช็คบัตรช้าและเสียเวลา แต่ React วาด component ใหม่นั้น
เร็วมากจนส่วนใหญ่ไม่มีใครรู้สึก
อย่าเอาไปเข้าใจว่า "ลืม useMemo = เว็บช้า" — มันจะกลายเป็นปัญหาก็ต่อเมื่อ
consumer มีเยอะ หรือแต่ละตัววาดแล้วหนัก (เช่น กราฟ ตารางยาว ๆ)
และที่สำคัญ: ตัวเลข render ในคอนโซลข้างบนไม่ได้แปลว่าเว็บช้าลงจริง
มันแค่ยืนยันว่า "วาดใหม่จริง" ถ้าจะบอกว่าช้าต้องวัดเวลาจริงอีกที
สรุปกฎที่จำง่าย: ถ้า value ของ Provider เป็น object หรือ array หรือ function
ให้ห่อด้วย useMemo / useCallback
· ถ้าเป็น string · number · boolean เช่น value="dark" ในสไลด์หน้า 53
ไม่ต้องห่อ เพราะค่าพวกนี้เทียบกันด้วยค่าตรง ๆ อยู่แล้ว
และย้ำจากบทที่ 75: React ในโปรเจกต์นี้เป็น 19.2.8 · next.config.ts ไม่ได้เปิด React Compiler ไว้
(ข้างในมีแค่คอมเมนต์ /* config options here */) — useMemo จึงยังจำเป็นอยู่ ไม่มีใครทำให้ฟรี
7 · กับดัก 3 — Provider ต้องเป็น client component
App Router ถือว่าทุกไฟล์เป็น Server Component จนกว่าจะเขียน 'use client'
· และ createContext กับ useContext เป็นของฝั่ง client ล้วน ๆ
ผมลองทำหน้าที่ลืมใส่ 'use client' แล้วเรียกดู:
// app/t76/server-hook/page.tsx — ไม่มี 'use client'
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
export default function ServerHookPage() {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
}
ผลรันจริง — HTTP 500 พร้อมข้อความนี้:
⨯ ./app/t76/server-hook/page.tsx:1:10
Error: You're importing a module that depends on `createContext` into a
React Server Component module. This API is only available in Client Components.
To fix, mark the file (or its parent) with the `"use client"` directive.
Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client
> 1 | import { createContext, useContext } from 'react';
| ^^^^^^^^^^^^^
2 |
3 | const ThemeContext = createContext('light');
4 |
Ecmascript file had an error
GET /t76/server-hook 500 in 385ms (next.js: 341ms, application-code: 44ms)
⚠️ ผลข้างเคียงที่ผมไม่ได้คาดไว้ และเจอตอนรันจริง
ตอนไฟล์นั้นยังอยู่ในโปรเจกต์ หน้าอื่นที่ไม่เกี่ยวกันเลยก็พังไปด้วย —
ผม curl ไป /t76/rerender ได้ HTTP 500 กลับมา ทั้งที่หน้านั้นเขียนถูกทุกอย่าง
เพราะมันเป็น compile error ของทั้งแอป ไม่ใช่ runtime error ของหน้าเดียว
พอลบไฟล์นั้นทิ้ง /t76/rerender กลับมา 200 ทันที
บทเรียนที่ใช้ได้จริง: ถ้า next dev พังทั้งเว็บพร้อมกันหมด
อย่าเพิ่งไล่แก้หน้าที่กำลังเปิดอยู่ — ให้ไปอ่านบรรทัดแรกของ error ว่ามันชี้ไปที่ไฟล์ไหน
ทางแก้ที่ถูกต้องไม่ใช่การใส่ 'use client' ที่หน้าเว็บ —
ให้ใส่ที่ไฟล์ context แล้วหน้าเว็บ (ซึ่งยังเป็น Server Component) import Provider มาครอบได้ตามปกติ
ดูที่ app/t76/page.tsx ในหัวข้อ 3 — ไฟล์นั้นไม่มี 'use client' เลย
แต่ครอบ <ThemeProvider> ได้ และ build ผ่านเป็น static (○ /t76 ในตารางผลลัพธ์)
เหตุผล: Server Component ส่ง Client Component ไปเป็น children ได้
สิ่งที่ห้ามคือการเรียก hook เองในไฟล์ที่เป็น server
app/t76/page.tsx ในหัวข้อ 3 ไม่มี 'use client' สักบรรทัด
แต่ครอบ <ThemeProvider> ได้ และ build ออกมาเป็น static (○ /t76 ในตาราง) —
เพราะอะไร
ตอบตัวเลือกที่ 1 · เส้นแบ่ง server/client อยู่ที่ "ไฟล์ไหนเรียก hook"
ไม่ใช่ "ไฟล์ไหนมี Provider อยู่ในหน้าจอ"
ตัวเลือกที่ 2 ไม่จริง — หัวข้อ 7 พิสูจน์แล้วว่าถ้าไฟล์ server แตะ createContext เอง
มันพังทันที ไม่มีใครเติมให้
ตัวเลือกที่ 3 ไม่จริง วิธี export ไม่เกี่ยวอะไรเลย · ตัวเลือกที่ 4 ก็ไม่จริง
useContext เป็น hook เหมือนกัน เรียกฝั่ง server ไม่ได้
ผลที่ได้จริงจากการทำแบบนี้: หน้าเว็บยังเป็น static ได้ (ไม่เสีย performance)
แต่กล่อง Provider กับ <UserBadge /> ข้างในกลายเป็น client — ได้ทั้งสองอย่าง
นี่คือเหตุผลที่คู่มือ Next.js บอกให้วาง 'use client' ให้ลึกที่สุดเท่าที่ทำได้
8 · กับดัก 4 — context ไม่ใช่ตัวแทนของ redux
พอเห็นว่า "ส่งค่าไปทั้งเว็บได้โดยไม่ต้องส่ง prop" คนมักสรุปทันทีว่า "งั้นเอา context เก็บ state ทั้งแอปเลยสิ ไม่ต้องใช้ Redux / Zustand แล้ว" — อันนี้ต้องระวัง
จากผลรันในหัวข้อ 6 เราเห็นแล้วว่า เมื่อค่าใน context เปลี่ยน consumer ทุกตัววาดใหม่หมด
ไม่มีการเลือกเฉพาะคนที่ใช้ field ที่เปลี่ยน
ถ้าเอา state ทั้งแอปยัดใส่ context ก้อนเดียว การกดปุ่มเปลี่ยนแค่ชื่อผู้ใช้
ก็จะทำให้ทุกหน้าจอที่แตะ context นั้นวาดใหม่พร้อมกันหมด
| ลักษณะของข้อมูล | เหมาะกับ context ไหม | เพราะอะไร |
|---|---|---|
| ธีม · ภาษา · ข้อมูลผู้ใช้ที่ล็อกอินอยู่ | เหมาะมาก | เปลี่ยนนาน ๆ ที และเปลี่ยนทีก็ตั้งใจให้ทั้งหน้าเปลี่ยนตามอยู่แล้ว |
| ค่าที่ตั้งครั้งเดียวตอนเปิดแอป เช่น base URL ของ API | เหมาะมาก | ไม่เปลี่ยนเลย จึงไม่มีปัญหา re-render ตั้งแต่แรก |
| ค่าในฟอร์มที่พิมพ์อยู่ · ตำแหน่งเมาส์ · ค่าที่เปลี่ยนถี่มาก | ไม่เหมาะ | เปลี่ยนทุกการพิมพ์หนึ่งตัวอักษร แล้วลากทั้ง subtree ให้วาดใหม่ตาม |
| state ทั้งแอปที่มีหลายสิบ field ปนกัน | ไม่เหมาะ | context เลือกไม่ได้ว่าใครสนใจ field ไหน · เครื่องมืออย่าง Redux / Zustand ทำเรื่องนี้ได้ |
| ข้อมูลที่ดึงมาจาก API แล้วต้อง cache · refetch · จัดการ loading | ไม่เหมาะ | context เก็บค่าได้อย่างเดียว ไม่มีเรื่อง cache หรือ retry ให้ · งานนี้เป็นของ data-fetching library |
วิธีคิดที่ใช้ตัดสินใจได้จริง: context แก้ปัญหา "ส่งค่ายังไง" ไม่ได้แก้ปัญหา "จัดการ state ยังไง"
ถ้าปัญหาของคุณคือ prop drilling → context ตอบโจทย์เลย
· ถ้าปัญหาคือ state ซับซ้อน มีหลายคนแก้ หลายที่ฟัง อยากได้ devtools ย้อนดูได้ → นั่นคนละปัญหา
- referential equality
- การเทียบว่า "เป็นวัตถุก้อนเดียวกันไหม" ไม่ใช่ "ข้างในเหมือนกันไหม" ·
React ใช้เกณฑ์นี้ (ผ่าน
Object.is) ตัดสินว่าค่า context เปลี่ยนหรือยัง — นี่คือรากของกับดักหัวข้อ 6 - Server Component / Client Component
- ใน App Router ทุกไฟล์เป็น Server Component จนกว่าจะเขียน
'use client'บรรทัดบนสุด · hook ทุกตัวรวมถึงuseContextใช้ได้เฉพาะฝั่ง client - Strict Mode
- โหมดของ React ตอน dev ที่ตั้งใจเรียก component สองรอบเพื่อจับ side effect ที่ไม่ควรมี · เป็นเหตุผลที่ log ในหัวข้อ 6 ขึ้นซ้ำสองบรรทัด และไม่เกิดตอน production
ทีมมีหน้า dashboard ที่ช่องค้นหาอัปเดตทุกตัวอักษรที่พิมพ์ และมีการ์ดสถิติ 30 ใบที่วาดกราฟใบละอัน · เพื่อนเสนอให้เอาข้อความในช่องค้นหาไปเก็บใน context ที่ครอบทั้งหน้า — ควรตอบว่าอะไร
ตอบตัวเลือกที่ 4 · ตัวเลือกที่ 1 ผิดตรงจุดสำคัญ — ผลรันในหัวข้อ 6 แสดงชัดว่า
memo กันไม่อยู่ ถ้า component นั้นเป็น consumer ของ context ที่เปลี่ยน
(LiteralChild ห่อ memo ไว้แต่ก็ยังวาดใหม่ #3–#6)
ตัวเลือกที่ 2 ผิดเพราะ useMemo ช่วยแค่ตอน "ค่าไม่ได้เปลี่ยนจริง" —
แต่ข้อความค้นหาเปลี่ยนจริงทุกตัวอักษร ก้อนใหม่จึงถูกสร้างอยู่ดี
ตัวเลือกที่ 3 แรงเกินไป context เก็บค่าที่เปลี่ยนได้ (ธีมในบทนี้ก็เปลี่ยนได้ด้วยปุ่ม) —
เกณฑ์จริงคือเปลี่ยนถี่แค่ไหน และมีคนฟังกี่คน ซึ่งคือสิ่งที่ตัวเลือกที่ 4 บอกให้ไปดูก่อน
9 · สรุปเป็นเช็คลิสต์ที่เอาไปใช้ได้เลย
| ขั้นตอน | ทำอะไร | ถ้าลืมจะเจออะไร (ของจริงจากบทนี้) |
|---|---|---|
| 1 | สร้างไฟล์ context แล้วใส่ 'use client' บรรทัดบนสุด |
You're importing a module that depends on `createContext` into a React Server Component module. — และพังทั้งเว็บ ไม่ใช่แค่หน้าเดียว |
| 2 | ประกาศ type ให้ชัด ค่าเริ่มต้นเป็น undefined |
ถ้าใส่ค่าเริ่มต้นแบบ "light" → หน้าขึ้น HTTP 200 พร้อมค่าปลอม ไม่มีใครเตือน |
| 3 | เขียน custom hook ที่มี if (!ctx) throw แล้วให้ทุกคนเรียกผ่านตัวนี้ |
ถ้าเรียก useContext ตรง ๆ จะได้ TypeError: Cannot destructure property … as it is undefined ที่ไม่บอกว่าลืมอะไร |
| 4 | รับ children ใน Provider ให้ครบ |
TS2304: Cannot find name 'children' — สไลด์หน้า 54 พลาดข้อนี้ |
| 5 | ถ้า value เป็น object / array / function ให้ห่อ useMemo |
consumer วาดใหม่ทุกครั้งที่แม่วาด แม้ค่าไม่เปลี่ยน (LiteralChild render #3…#6) |
| 6 | ครอบ Provider ให้อยู่เหนือคนที่จะใช้ ไม่ใช่ในไฟล์เดียวกับที่เรียก hook | ได้ค่าเริ่มต้นแทนค่าจริง — สไลด์หน้า 55 เตือนเรื่องนี้ไว้ตรง ๆ |
ถ้าจำได้แค่ประโยคเดียวจากบทนี้ ให้จำอันนี้:
context ไม่ได้ทำให้ค่าเดินทางเร็วขึ้น มันแค่ทำให้เราไม่ต้องเขียนทางเดินเอง —
และค่าที่ผิดจะเงียบกว่าค่าที่ไม่มี
🛠 ภารกิจ — ทำให้ "ค่าปลอมเงียบ ๆ" กลายเป็น error ที่ส่งเสียง
ทำในโปรเจกต์ Next.js ของคุณเอง (ยังไม่มี: npx create-next-app@latest) ·
ข้อ 2 กับ 3 คือหัวใจของบทนี้ ห้ามข้าม เพราะมันคือการเห็นกับตาว่า
"พัง" กับ "ดูเหมือนใช้ได้" ต่างกันยังไง
-
สร้าง
components/ThemeCtx.tsx· บรรทัดแรก'use client'· ข้างในทำ แบบสไลด์หน้า 54 ก่อน คือcreateContext('light')แล้วเขียน hookuseTheme()ที่มีif (!context) throw new Error(...)· แล้วทำ component เล็ก ๆ ที่โชว์ค่าที่ได้ออกมาบนหน้าจอ -
ทำหน้าที่เรียก component นั้น โดยไม่ครอบ Provider เลย ·
รัน
npx next devแล้วเปิดดู · จดว่าได้ HTTP อะไร และหน้าจอขึ้นข้อความว่าอะไร (guard ที่คุณเพิ่งเขียนทำงานไหม?) -
แก้บรรทัดเดียว: เปลี่ยนเป็น
createContext<{ mode: string } | undefined>(undefined)· โหลดหน้าเดิมซ้ำ · จดว่าเปลี่ยนไปยังไง แล้วเทียบกับข้อ 2 -
ครอบ Provider ให้ถูกต้อง แล้วรัน
npx next build· ดูตารางRoute (app)ว่าหน้าของคุณขึ้น○หรือƒ· ลองถอด Provider ออกอีกครั้งแล้ว build ใหม่ — คราวนี้ build จะเป็นยังไง -
ลบ
'use client'ออกจากไฟล์ context โดยที่page.tsxยังเป็น Server Component เหมือนเดิม แล้วโหลดหน้าใหม่ · เปิดหน้าอื่นที่ไม่เกี่ยวข้องกันเลยดูด้วย
แล้วลองอีกท่า: เติม'use client'ไว้ที่page.tsxแทน (ไฟล์ context ยังไม่มี) — คราวนี้ผลต่างไปไหม? -
โบนัส: ทำหน้าแบบหัวข้อ 6 · ปุ่มที่เปลี่ยน state ที่ไม่เกี่ยวกับ context ·
ลูกห่อ
memo· Provider ตัวหนึ่งใช้value={{ n }}อีกตัวใช้useMemo· กดปุ่ม 2 ที แล้วนับ log
ข้อ 2 — ต้องได้ HTTP 200 และหน้าขึ้นค่า light ตามปกติ
ไม่มี error สักตัว · guard ที่คุณเขียนไม่ทำงาน
เพราะ "light" เป็น truthy · ถ้าคุณได้ 500 แปลว่าเผลอใส่ค่าเริ่มต้นเป็น
undefined หรือ "" ไปแล้ว ให้ย้อนกลับไปทำข้อ 1 ให้ตรง
ข้อ 3 — คราวนี้ต้องได้ HTTP 500 พร้อมข้อความที่คุณเขียนเอง
และ code frame ที่ชี้ชื่อไฟล์กับเลขบรรทัดของ throw · นี่คือความต่างทั้งหมด:
บรรทัดเดียวเปลี่ยนบั๊กเงียบให้กลายเป็นบั๊กที่บอกทางแก้
ข้อ 4 — ครอบถูกแล้วควรได้ ○ (static) · พอถอด Provider ออกแล้ว build ใหม่
จะไม่ใช่แค่ "หน้านั้นพัง" แต่ next build ล้มทั้งโปรเจกต์ ·
ในเครื่องผมได้
Error occurred prerendering page "/t76/no-provider" ตามด้วย
Export encountered an error … exiting the build และ
⨯ Next.js build worker exited with code: 1
ทางออกตอนอยากเก็บหน้าพังไว้ดูคือใส่ export const dynamic = 'force-dynamic'
(นั่นคือเหตุผลที่หน้ากับดักในบทนี้ขึ้น ƒ ในตาราง)
ข้อ 5 ท่าแรก — ต้องพังแบบหัวข้อ 7 คือ
You're importing a module that depends on `createContext` into a React Server Component module
· และหน้าที่ไม่เกี่ยวข้องเลยก็ต้องพังไปด้วย เพราะเป็น compile error ของทั้งแอป
ไม่ใช่ของหน้าเดียว · ในเครื่องผม /t76 กับ /t76/drilling ได้ 500 พร้อมกันทั้งคู่
คราวนี้ error จะแถม Import traces: มาให้ด้วย ซึ่งไล่ให้ดูเลยว่า
./components/…/theme.tsx ถูกลากเข้ามาผ่าน ./app/…/page.tsx —
อ่านตรงนี้แล้วรู้ทันทีว่าต้องไปใส่ 'use client' ที่ไฟล์ไหน
ข้อ 5 ท่าที่สอง — กลับมาได้ HTTP 200 ทำงานปกติ
เพราะพอ page.tsx ประกาศ 'use client' ทุกอย่างที่มัน import
ก็ถูกนับเป็นฝั่ง client ไปด้วย · แต่นี่ไม่ใช่ท่าที่ควรใช้ —
คุณเพิ่งลากทั้งหน้าไปเป็น client เพื่อแก้ปัญหาของไฟล์เดียว
· ที่ถูกคือใส่ 'use client' ที่ไฟล์ context แล้วปล่อยให้หน้าเป็น server ต่อไป
บทเรียนของข้อนี้: "มันหายพัง" ไม่ได้แปลว่า "แก้ถูกที่"
ข้อ 6 — ตัวที่ใช้ value={{ n }} ต้องมี log โผล่ทุกครั้งที่กด
ส่วนตัวที่ใช้ useMemo ต้องเงียบสนิท ·
ถ้าเลข render ของคุณขึ้นทีละ 2 ไม่ใช่ทีละ 1 นั่นคือ Strict Mode ตอน dev
ไม่ใช่บั๊ก · และถ้าทั้งคู่เงียบ แปลว่าปุ่มของคุณไม่ได้ทำให้ component แม่วาดใหม่จริง
ลองเช็คว่า setState อยู่ในแม่ตัวเดียวกับ Provider หรือเปล่า
หมายเหตุเรื่องวิธีตรวจ (เขียนไว้ให้ตรวจสอบซ้ำได้):
ตอนผมรัน มี dev server ตัวอื่นจองโฟลเดอร์ทดสอบอยู่แล้ว และ Next.js ไม่ยอมให้เปิด
next dev ซ้อนกันสองตัวในโฟลเดอร์เดียว (Another next dev server is already running.)
· ผมจึงคัดลอกโปรเจกต์ทั้งชุดไปอีกโฟลเดอร์หนึ่งโดยใช้ node_modules · package.json ·
tsconfig.json · next.config.ts ชุดเดียวกันเป๊ะ แล้วรันที่พอร์ต 376
เวอร์ชันทุกตัวจึงเท่ากับที่แจ้งไว้ในกล่องบนสุดของหน้า และคุณรันซ้ำเองได้ด้วยโค้ดที่โชว์ไว้ทั้งหมด
อ้างอิง
- React —
useContext— อ่านหัวข้อ Caveats ที่เขียนไว้ตรง ๆ ว่า Provider ที่ return จาก component เดียวกัน จะไม่ถูกมองเห็นโดยuseContextใน component นั้น (ตรงกับที่สไลด์หน้า 55 เตือน) แล้วอ่านหัวข้อ Optimizing re-renders when passing objects and functions ต่อ ซึ่งเป็นเรื่องเดียวกับหัวข้อ 6 ของบทนี้ - React —
createContext— อ่านย่อหน้าที่อธิบาย parameterdefaultValueที่บอกว่าค่านี้ถูกใช้ "ก็ต่อเมื่อไม่มี Provider ที่ตรงกันอยู่เหนือขึ้นไป" — และดูตัวอย่างที่เขาแนะนำให้ใส่ค่าที่ "never changes" · เทียบกับผลรันในหัวข้อ 5 ว่าทำไมค่าเริ่มต้นที่เป็น truthy ถึงอันตราย - React — Passing Data Deeply with Context —
อ่านหัวข้อ Before you use context ที่เขาแนะนำให้ลอง 2 ทางก่อน
(ส่ง prop ตรง ๆ · หรือส่ง JSX ไปเป็น
children) แล้วค่อยใช้ context — เป็นคำตอบเดียวกับหัวข้อ 8 ของบทนี้ในเรื่อง "context ไม่ใช่ยาครอบจักรวาล" - Next.js —
use clientdirective — ลิงก์นี้คือลิงก์ที่ตัว error เองแนะนำให้ไปอ่าน (ดูข้อความ error ในหัวข้อ 7) · อ่านหัวข้อที่ว่าด้วยการวาง'use client'ให้ลึกที่สุดเท่าที่ทำได้ เพื่อเข้าใจว่าทำไมเราถึงใส่ที่ไฟล์ context ไม่ใช่ที่page.tsx - React —
memo— อ่านหัวข้อ Caveats ข้อที่พูดถึง context ซึ่งระบุว่าmemoไม่กันการ re-render ที่เกิดจาก context ที่ component นั้น subscribe อยู่ — นี่คือคำอธิบายอย่างเป็นทางการของสิ่งที่คอนโซลในหัวข้อ 6 แสดงให้เห็น - React —
StrictMode— อ่านหัวข้อ Fixing bugs found by double rendering in development เพื่อเข้าใจว่าทำไม log ในหัวข้อ 6 ถึงขึ้นซ้ำสองบรรทัดทุกครั้ง และทำไมมันไม่ใช่บั๊ก