Cheat sheet · Frontend · Next.js

Next.js — App Router · component · Tailwind · hooks แผ่นเดียวจบ

คู่กับ บทเรียน 69–78 · ทุกผลรันในหน้านี้มาจากโปรเจกต์ Next.js 16.3.4 ตัวจริง ไม่ได้พิมพ์จากความจำ

หน้านี้ไม่ได้เขียนไว้ให้อ่านรวดเดียว — เขียนไว้ให้เปิดตอนมือติดคีย์บอร์ด
ถ้ากำลังรีบ: หัวข้อ 10 (ตารางแปล error) · หัวข้อ 11 (ตารางกับดักรวมทั้งกลุ่ม) และหัวข้อ 7 (ตาราง hooks ทั้งห้า) คือสามอันที่จะได้ใช้บ่อยที่สุด
ถ้าเพิ่งมาจาก Go ให้เริ่มที่หัวข้อ 3 — จุดที่ App Router กลับหัวจาก Gin อยู่ตรงนั้น
ถ้า next build เพิ่งพัง ให้ข้ามไปหัวข้อ 4 ก่อนเลย เกินครึ่งของ error ในกลุ่มนี้คือเรื่องเดียวกันหมด

🧰 ตรวจด้วยอะไร เวอร์ชันไหน — ทุกบรรทัดผลรันในหน้านี้มาจาก

next                  16.3.4    # Turbopack เป็นค่าปริยายของ Next 16
react / react-dom     19.2.8
typescript            5.9.3
tailwindcss           4.3.3     # ผ่าน @tailwindcss/postcss 4.3.3
eslint                9         # + eslint-config-next 16.3.4
node                  v24.14.0

# โปรเจกต์สร้างด้วย:
npx create-next-app@latest --ts --tailwind --eslint --app --no-src-dir

ทุก error code · ทุกก้อน HTML · ทุกตัวเลขในหน้านี้ ก็อปมาจากผลรันจริงในบทเรียน 69–78 ถ้าเวอร์ชันบนเครื่องคุณไม่ตรง ข้อความอาจต่างกันได้จริง — ให้เชื่อผลรันบนเครื่องคุณ ไม่ใช่ตัวหนังสือในหน้านี้

สารบัญ

  1. คำสั่งที่ใช้จริงทุกวัน
  2. โครงโฟลเดอร์ที่ได้มา — ไฟล์ไหนทำอะไร
  3. path ↔ URL (+ เทียบ Gin)
  4. Server Component vs Client Component
  5. component + props + children + spread
  6. Tailwind — utility แปลกลับเป็น CSS
  7. ตาราง hooks ทั้งห้า
  8. useContext — เช็คลิสต์ 6 ขั้น
  9. custom hook — กฎ 4 ข้อ
  10. ตารางแปล error ที่เจอบ่อย
  11. ตารางกับดักรวมทั้งกลุ่ม
  12. อ้างอิง

1 · คำสั่งที่ใช้จริงทุกวัน

อยากทำอะไรพิมพ์ที่ต้องรู้
สร้างโปรเจกต์ใหม่ npx create-next-app@latest ขีดกลาง · เป็นบรรทัดที่เอกสารทางการใช้ · เลือก Yes, use recommended defaults ได้เลย
เหมือนกัน แต่ผ่าน npm npm create next-app@latest เว้นวรรค · create คือชื่อคำสั่ง ส่วน next-app@latest คือของที่ส่งให้มัน
รันตอนพัฒนา npm run dev · npx next dev ค่าปริยาย http://localhost:3000 · เปลี่ยนพอร์ตด้วย -p 3073 · ต้องมีคำว่า run เพราะ dev เป็นชื่อสคริปต์ ไม่ใช่คำสั่งของ npm
build ของจริง npx next build รัน TypeScript ให้ด้วย · พิมพ์ตาราง Route ทั้งเว็บออกมาให้ดู — อยากรู้ว่าเว็บมีหน้าอะไรบ้าง ดูจากตารางนี้
รันของที่ build แล้ว npx next start ต้อง next build ให้เสร็จก่อน · ใช้ตัวนี้เทียบว่า production ต่างจาก dev ยังไง (เช่น effect รันครั้งเดียว ไม่ใช่สองครั้ง)
ตรวจ type อย่างเดียว npx tsc --noEmit ผ่านแล้วไม่พิมพ์อะไรเลย · ต้องดู exit code เอง → npx tsc --noEmit; echo "rc=$?"
lint npx eslint app components
npm run lint
สคริปต์ lint ที่ create-next-app ใส่มาให้คือ "eslint" เฉย ๆ · npx next lint ไม่มีแล้วใน Next 16 (ดูกล่องล่าง)
เช็คเวอร์ชัน npx next --version · node --version
node -e "console.log(require('react/package.json').version)"
คำถามแรกทุกครั้งที่โค้ดจากสไลด์/บล็อก/AI พัง คือ “อันนี้เขียนไว้ตอน React เวอร์ชันอะไร” ไม่ใช่ “ผมพิมพ์ผิดตรงไหน”
ดูว่า server ส่ง HTML อะไรมาจริง curl -s http://localhost:3000/
curl -o /dev/null -w "%{http_code}\n" …
curl ไม่รัน JavaScript — อะไรที่เห็นใน curl คือของที่ server ส่งมาจริง ความที่มันไม่รัน JS นั่นแหละคือเหตุผลที่ใช้เป็นหลักฐานได้
⚠ สไลด์หน้า 14 บรรทัดแรกใช้ไม่ได้ — ขาดเว้นวรรคหนึ่งที่
$ npm create-next-app@latest        ← ตามสไลด์
Unknown command: "create-next-app@latest"

To see a list of supported npm commands, run:
  npm help

เหตุผลอยู่ในหน้าช่วยเหลือของ npm เอง:

$ npm create --help
npm init <package-spec> (same as `npx create-<package-spec>`)

— npm เติม create- ให้เอง เขียนติดกันจึงกลายเป็นชื่อคำสั่งที่ไม่มีอยู่ · กฎที่จำได้ยาว: npx = ขีดกลาง · npm create / pnpm create = เว้นวรรค · บรรทัดของ yarn กับ pnpm ในสไลด์ถูกอยู่แล้ว

⚠ npx next lint ไม่มีแล้วใน Next 16.3.4
$ npx next lint
Invalid project directory provided, no such directory: /…/next-sandbox/lint

มันเอาคำว่า lint ไปเข้าใจว่าเป็นชื่อโฟลเดอร์ · ของจริงต้องเรียก eslint ตรง ๆ — และต้องรันมันทุกครั้ง ไม่ใช่แค่ next build เพราะกับดักหลายอันในกลุ่มนี้ build ปล่อยผ่านแต่ eslint จับได้

2 · โครงโฟลเดอร์ที่ได้มา — ไฟล์ไหนทำอะไร

$ ls -1a          # ผลรันจริงหลัง create-next-app (next 16.3.4)
.
..
.git              ← create-next-app สั่ง git init ให้เอง
.gitignore
AGENTS.md         ← ติดมาด้วยในเวอร์ชันนี้
CLAUDE.md         ← ติดมาด้วยในเวอร์ชันนี้
README.md
app
eslint.config.mjs
next-env.d.ts
next.config.ts
node_modules
package-lock.json
package.json
postcss.config.mjs
public
tsconfig.json

$ ls -1 app
favicon.ico
globals.css
layout.tsx
page.tsx
ของทำอะไรแตะได้ไหม
app/โฟลเดอร์เดียวที่กลายเป็น URL · Next.js อ่านโครงข้างในเป็นแผนที่เส้นทางที่ทำงานหลักของคุณ
app/layout.tsxroot layout — ครอบทุกหน้าในเว็บ · เป็นที่เดียวที่มีแท็ก <html> กับ <body> · Next.js บังคับให้ต้องมีแตะได้ แต่ของที่ใส่ตรงนี้ทุกหน้าต้องแบก — ใส่ให้ต่ำที่สุดเท่าที่ทำได้
app/page.tsxหน้าแรก /แตะได้
app/globals.cssบรรทัดแรกคือ @import "tailwindcss"; · ถูก import ที่ app/layout.tsx ที่เดียว แล้วครอบทุกหน้าตั้งค่า Tailwind ทั้งหมดอยู่ในบล็อก @theme ของไฟล์นี้
public/ไฟล์นิ่ง ๆ ที่เสิร์ฟตรง ๆแตะได้ · ไฟล์ที่นี่ไม่ใช่ component
next.config.tsค่าตั้งของ Next.js · ค่าเริ่มต้นคือว่างเปล่า — ข้างในมีแค่ /* config options here */เปิดดูเมื่ออยากรู้ว่า React Compiler เปิดอยู่ไหม (หาคำว่า reactCompiler)
postcss.config.mjsตัวต่อ Tailwind เข้ากับ build — "@tailwindcss/postcss": {}ลบบรรทัดนี้แล้วไม่มี error แต่สไตล์หายทั้งเว็บ
tsconfig.jsonมี "paths": { "@/*": ["./*"] } · "resolveJsonModule": true · "isolatedModules": true มาให้แล้วสามค่านี้อธิบาย @/ · การ import ไฟล์ .json ตรง ๆ · และ TS1205 ในหัวข้อ 5
eslint.config.mjsกฎตัวจริงของ eslint · ส่วนขยายใน VS Code แค่เอาผลมาแสดง กฎอยู่ที่ไฟล์นี้ลง extension แล้วเส้นหยักไม่ขึ้น ให้มาดูว่ามีไฟล์นี้ไหมก่อน
.next/ยังไม่มีตอนแรก · โผล่หลังรันครั้งแรก · เป็นของที่ Next.js สร้างเอง (มี .next/server/app/<route>.html ที่ prerender ไว้)อย่าแก้ · อยู่ใน .gitignore แล้ว · เปิดอ่านได้ ใช้พิสูจน์ว่า server render จริง
components/ · containers/ไม่ใช่โฟลเดอร์พิเศษของ Next.js — ตั้งชื่ออะไรก็ได้ · สไลด์หน้า 20 เสนอว่า components/ = ชิ้นที่ไม่จำอะไรเอง · containers/ = ชิ้นที่จำสถานะเองและประกอบตามงานจริงเป็นข้อตกลงของทีม ไม่ใช่กฎของเครื่อง · ใช้ที่เดียว→วางข้างหน้าที่ใช้ · ใช้หลายที่→ยกออกมา

ไฟล์ชื่อพิเศษใต้ app/

ชื่อไฟล์เป็น URL ของตัวเองไหมหน้าที่ · ข้อบังคับ
page.tsx ✅ ใช่ — มันคือสิ่งที่ทำให้โฟลเดอร์กลายเป็นหน้าเว็บ ต้อง export default (ไม่ใช่ export เฉย ๆ) และของที่ export ต้องเป็นฟังก์ชันที่คืน JSX · ชื่อฟังก์ชันตั้งว่าอะไรก็ได้ Next.js ไม่ได้ดูชื่อ
layout.tsx ❌ ไม่ · มันคือ “กรอบ” รับ children แล้ววาง {children} ตรงที่อยากให้หน้ามาโผล่ · ลืมเขียน {children} = เนื้อหาหายเงียบ ๆ ไม่มีใครฟ้อง · ครอบทุกหน้าที่อยู่ใต้มันทุกชั้น · ซ้อนกันหลายชั้นได้
not-found.tsx — สร้างเองแล้วมันจะมาแทนหน้า 404 มาตรฐาน · บรรทัด /_not-found ที่โผล่ในตาราง build คือของที่ Next.js แถมมาให้ทุกโปรเจกต์ ไม่ใช่ของที่คุณทำพลาด
ไฟล์ชื่ออื่น (note.tsx · ReportTable.tsx) ❌ ไม่ วางไว้ในโฟลเดอร์ route ได้ตามปกติ ไม่ใช่ error — next build ไม่บ่นสักคำ แค่ไม่มีบรรทัดของมันในตาราง route

3 · path ↔ URL (+ เทียบ Gin)

กฎมีสองข้อเท่านั้น: (1) ชื่อโฟลเดอร์ = ชิ้นส่วนหนึ่งของ URL · (2) โฟลเดอร์จะเป็นหน้าเว็บได้ก็ต่อเมื่อในนั้นมี page.tsx

ไฟล์ในโปรเจกต์URL ที่ได้ผล curl จริง
app/page.tsx/✅ 200 — หน้าแรก
app/about/page.tsx/about✅ 200
app/blog/hello/page.tsx/blog/hello✅ 200 — ซ้อนกี่ชั้นก็ได้
app/dashboard/settings/page.tsx/dashboard/settings✅ 200 · app/ ไม่นับเป็นชิ้นส่วนของ URL
app/blog/ (ไม่มี page.tsx ของตัวเอง)—❌ 404 — โฟลเดอร์ที่เป็นแค่ทางผ่านไม่ใช่หน้า · จุดที่คนพลาดมากที่สุด
app/secret/note.tsx—❌ 404 — ชื่อไฟล์ไม่ใช่ page.tsx
app/layout.tsx—ไม่เป็น URL ของตัวเอง · มันไปครอบหน้าอื่นแทน
$ npx next build          # หลักฐานที่หนักแน่นกว่า curl — Next.js พิมพ์รายการ route ออกมาเอง
✓ Compiled successfully

Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /about
└ ○ /blog/hello           ← ไม่มี /blog และไม่มี /secret ในตารางนี้

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

เทียบกับ Gin ที่เพิ่งเรียนมา (บท 38)

เรื่องGinNext.js App Router
บอก path ยังไง พิมพ์เป็นสตริง r.GET("/users/:id", h) ตั้งชื่อโฟลเดอร์ — ไม่มีสตริงให้พิมพ์ และไม่มีที่ให้เขียนด้วย
อยากรู้ว่ามี route อะไรบ้าง อ่านไฟล์ที่รวม r.GET/r.POST ไว้ ดูโครงโฟลเดอร์ หรืออ่านตารางจาก next build
พิมพ์ path ผิด คอมไพล์ผ่าน แล้วได้ 404 ตอนรัน ได้ 404 เหมือนกัน — แต่เห็นตั้งแต่ตอนตั้งชื่อโฟลเดอร์ใน editor
สอง route ชื่อชนกัน เป็นไปได้ — ต้องระวังลำดับที่ลงทะเบียน เป็นไปไม่ได้ — ระบบไฟล์ไม่ยอมให้มีสองโฟลเดอร์ชื่อเดียวกันในที่เดียวกัน
ย้าย route ไป path ใหม่ แก้สตริงในโค้ด เปลี่ยนชื่อโฟลเดอร์ · ไม่ต้องแตะโค้ดข้างในเลยสักบรรทัด
ประกาศ route ในไฟล์ config มี (ไฟล์ที่รวม handler) ไม่มี — next.config.ts ของโปรเจกต์ใหม่ว่างเปล่า แต่ทุก route ก็ใช้ได้
✅ layout ไม่ถูกวาดใหม่ตอนเปลี่ยนหน้า — แต่มีข้อยกเว้นที่ต้องจำคู่กัน
// ปุ่มนับเลขที่วางไว้ใน layout · กดจนเลขเป็น 2 แล้วกดลิงก์ไปอีกหน้า
{ "path": "/about", "h1": "PAGE: /about",
  "counter": "layout counter = 2", "sameButtonNode": true }   ← ปุ่มก้อนเดิมในหน้าเว็บ

// แต่พอโหลดหน้าใหม่ทั้งหน้า (F5)
{ "path": "/about", "counter": "layout counter = 0" }          ← เริ่มจากศูนย์

กรอบไม่ถูกถอดเฉพาะตอนเดินอยู่ในเว็บผ่านลิงก์ · F5 คือการโยนทุกอย่างทิ้งแล้วเริ่มใหม่ — นี่คือเหตุผลที่เมนูควรใช้ <Link> ไม่ใช่ <a> ธรรมดา

4 · Server Component vs Client Component

ใน App Router ทุกไฟล์เป็น Server Component โดยปริยาย จนกว่าจะเขียน 'use client' บรรทัดบนสุด · เกินครึ่งของ error ในกลุ่มนี้คือเส้นแบ่งเส้นนี้เส้นเดียว

อยากทำอะไรServer Component (ค่าปริยาย)Client Component ('use client')
useState · useEffect ❌ build พัง — This API is only available in Client Components. ✅ ได้
createContext · useContext ❌ build พัง เหมือนกัน · และพังทั้งแอป ไม่ใช่แค่หน้าเดียว ✅ ได้
useMemo · useCallback · useId ✅ ไม่ error จริง ๆ — สามตัวนี้อยู่ในรายการ export ฝั่ง server ของ react 19.2.8 · แต่ไม่ได้ประโยชน์ เพราะ Server Component วาดครั้งเดียวต่อ request ไม่มีรอบถัดไปให้จำ ✅ ได้ และได้ประโยชน์จริง
onClick และ event ทุกชนิด ❌ ไม่มีการกดปุ่มบนเซิร์ฟเวอร์ ✅ ได้
ถูกรันบนเซิร์ฟเวอร์เพื่อทำ HTML ตั้งต้น ✅ ใช่ (นั่นคืองานของมัน) ✅ ใช่ ก็ยังถูกรันบนเซิร์ฟเวอร์หนึ่งรอบอยู่ดี — ต้นตอของ hydration mismatch
ส่ง JavaScript ไปเบราว์เซอร์ ไม่ส่ง ส่ง — นี่คือความหมายจริงของ 'use client'
ครอบ Client Component เป็น children ✅ ได้ · หน้าที่ไม่มี 'use client' เลยก็ครอบ <ThemeProvider> ได้ และยัง build เป็น ○ (Static) ได้ด้วย —
เป็นหน้า ○ (Static) ✅ ✅ ได้เหมือนกัน — 'use client' ไม่ได้แปลว่า “ห้าม prerender”

ต้องใส่ 'use client' เมื่อไหร่ และตรงไหน

❌ วางใต้ import — build ไม่ผ่าน

import { useState } from 'react';
'use client';
Error: The "use client" directive must be
placed before other expressions.
Move it to the top of the file to resolve
this issue.
(+ error ตัวเดิมของ useState พ่วงมาด้วย = 2 errors)

✅ บรรทัดแรกสุด — build ผ่าน

'use client';

import { useState } from 'react';
✓ Compiled successfully
(ไม่มี error · route ของหน้านั้นขึ้นในตาราง Route ตามปกติ)
$ npx next build          # ไฟล์ที่ลืม 'use client' — ข้อความเต็ม ๆ ที่ได้จริง
> Build error occurred
Error: Turbopack build failed with 1 error:
./components/CustomerList.tsx:1:19
Error: You're importing a module that depends on `useState` 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 { useMemo, useState, type ChangeEvent } from "react";
    |                   ^^^^^^^^
Import trace:
  Server Component:
   ./components/CustomerList.tsx       ← แปะ 'use client' ที่ไฟล์นี้ (ไฟล์ที่ import hook เข้ามาจริง)
   ./app/page.tsx                      ← ไม่ใช่ที่นี่ · นี่แค่หน้าที่บังเอิญเรียกมัน

อ่าน trace ให้ถูกทาง: ไฟล์ที่ต้องแก้คือไฟล์เดียวกับที่บรรทัดพาธใต้ Turbopack build failed ชี้อยู่ (./components/CustomerList.tsx:1:19) ซึ่งเป็นบรรทัดแรกของ Import trace · บรรทัดถัด ๆ ไปคือหน้าที่ลากมันเข้ามา ไม่ใช่ที่ที่ต้องแปะ — แปะที่นั่นก็หายพังเหมือนกัน แต่คุณเพิ่งลากทั้งหน้าไปเป็น client เพื่อแก้ปัญหาของไฟล์เดียว

⚠ สี่ข้อที่ต้องจำให้ขึ้นใจเรื่อง 'use client'
  1. tsc --noEmit จับ error ตัวนี้ไม่ได้เลย — เส้นแบ่ง server/client เป็นกฎของ Next.js ไม่ใช่ของ TypeScript · VS Code จะไม่ขีดเส้นแดงให้ · “ไม่มีเส้นแดง” ไม่ได้แปลว่า “โค้ดใช้ได้” ในโปรเจกต์ Next.js
  2. มันบ่นตอน import ไม่ใช่ตอนเรียก — แค่ import ทิ้งไว้เฉย ๆ ก็พังแล้ว
  3. ติดครั้งเดียวคุ้มทั้งกิ่ง — ทุก component ที่ไฟล์นั้น import เข้ามากลายเป็น client ตามไปด้วย · เพราะงั้นอย่าแปะไว้บนสุดของต้นไม้ ให้แปะที่ component เล็กที่สุดที่ต้องใช้ hook จริง ๆ แล้ว JavaScript ที่ส่งไปเบราว์เซอร์จะน้อยลง
  4. ลืมใส่ที่ไฟล์ที่หน้าอื่นก็ import อยู่ = พังทั้งเว็บพร้อมกัน เพราะเป็น compile error ของทั้งแอป · อย่าเพิ่งไล่แก้หน้าที่กำลังเปิดอยู่ ให้อ่านบรรทัดแรกของ error ว่าชี้ไปที่ไฟล์ไหน

5 · component + props + children + spread

component คือฟังก์ชันธรรมดาที่รับ argument ได้ตัวเดียว (object ก้อนเดียวชื่อ props) แล้วคืน JSX · ไม่มีอะไรพิเศษกว่าฟังก์ชันใน Go นอกจากข้อตกลงสองข้อนั้น

// components/Bases/Button.tsx — สไลด์หน้า 22 ทุกตัวอักษร
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

const Button = (props: ButtonProps) => {
	const { children, ...restProps } = props;
	return <button {...restProps}>{children}</button>;
};

export default Button;
ชิ้นส่วนคืออะไรที่ต้องรู้
children ของที่อยู่ระหว่างแท็กเปิดกับแท็กปิด — React ยัดเข้า props ให้เองในช่องชื่อนี้ ไม่ต้องประกาศเอง มันติดมากับ ButtonHTMLAttributes อยู่แล้ว · ลืมเอาไปวางแล้ว tsc ไม่ฟ้อง — ได้ <button type="button"></button> ปุ่มเปล่า
const { children, ...restProps } = props rest — “ที่เหลือทั้งหมดยัดใส่กล่องนี้” อยู่ฝั่งซ้ายของ = หรือใน pattern การแกะ
<button {...restProps}> spread — “แกะกล่องออกแล้วโปรยของทุกชิ้นลงตรงนี้” ผลคือปุ่มรับ onClick · disabled · type · title ได้ครบ โดยไม่ต้องเขียนเพิ่มสักบรรทัด
variant?: "contained" | "outlined" | "text"
& React.ButtonHTMLAttributes<…>
intersection type — “ช่องของเรา บวกกับ ทุกอย่างที่ปุ่ม HTML รับได้” & = และ · | = อย่างใดอย่างหนึ่ง (อย่าสลับ) · ? = ไม่ส่งมาก็ได้
const { variant = "contained", … } = props ค่าตั้งต้นตอน destructure ทำงานเฉพาะตอนค่าเป็น undefined — ส่ง null หรือ "" มา ค่าตั้งต้นไม่ทำงาน
components/Bases/index.ts (barrel) ไฟล์ที่ไม่ทำอะไรนอกจากรวบของจากไฟล์ข้าง ๆ มา export ใหม่ ทำให้เขียน import { Button } from "@/components/Bases" ได้ · ไม่ใช่เวทมนตร์ — index เป็นแค่ข้อตกลงว่า “ชี้มาที่โฟลเดอร์ ให้เปิดไฟล์ชื่อนี้” · ลบทิ้งได้ TS2307 ทันที
@/components/… path alias — ไม่ใช่ของ Next.js แต่เป็นของ tsconfig.json "paths": { "@/*": ["./*"] } · ไฟล์อยู่ลึกแค่ไหนก็เขียน import เหมือนกันหมด ไม่ต้องนับ ../../..
ไม่มี import React แต่เขียน React.ButtonHTMLAttributes ได้ @types/react ประกาศชื่อ React ไว้เป็น namespace ระดับ global แล้ว มันเป็นแค่ “ชื่อของกล่อง type” ไม่ใช่ตัวแปรที่ต้องมีอยู่ตอนรัน · ลบ import React ออกแล้ว tsc ยังผ่าน

ตัวอักษรตัวแรกของชื่อ component — กฎที่เปลี่ยนความหมายทั้งหมด

<Button>  ตัวใหญ่ → React อ่านว่า "เรียกฟังก์ชันชื่อ Button ที่อยู่ในไฟล์นี้"
<button>  ตัวเล็ก → React อ่านว่า "สร้างแท็ก HTML ชื่อ button"
                    ↑ ไม่สนใจเลยว่าคุณจะประกาศ const button ไว้ข้างบนหรือเปล่า
เขียนแบบนี้TypeScriptผลจริงที่ได้
<Button> ตัวใหญ่ผ่าน✅ เรียก component ของคุณ
<button> ตัวเล็ก + ชื่อตรงกับแท็ก HTML จริง
(button · span · form · section · header)
ผ่าน ไม่ฟ้องอะไรเลย · next build ก็ผ่าน ☠️ สร้างแท็ก HTML · ฟังก์ชันของคุณไม่ถูกเรียกเลยสักครั้ง
curl ได้ <main><button>สวัสดี</button></main> — ไม่มีดาว ไม่มี <span>
<myButton> ตัวเล็ก + ไม่ตรงแท็กไหน TS2339 ❌ build ไม่ผ่าน — โชคดีที่พังตั้งแต่แรก
✅ กฎที่ปลอดภัยเสมอ

ตั้งชื่อไฟล์ให้ตรงกับชื่อ component และขึ้นต้นด้วยตัวพิมพ์ใหญ่ทั้งคู่ (PriceTag.tsx ข้างในมี PriceTag) แล้วคุณจะไม่มีวันเจอ TS2339 ตัวนี้ · ในงานจริง เวลาแก้ UI แล้วหน้าเว็บไม่เปลี่ยนตามที่คิด ให้เช็คตัวอักษรตัวแรกก่อน ก่อนจะไปนั่งไล่หา cache หรือ build system

type ในไฟล์ barrel — กฎจริงคือ “ต้องมีอย่างน้อยหนึ่งที่”

บรรทัด importบรรทัด exportผลจริงจาก npx tsc --noEmit
{ type ButtonProps }export type { … }✅ ผ่าน (exit 0) — แบบที่สไลด์เขียน
{ type ButtonProps }export { … }✅ ผ่าน (exit 0) — ไม่ error
{ ButtonProps }export type { … }✅ ผ่าน (exit 0)
{ ButtonProps }export { … }❌ TS1205 — แถวเดียวที่พัง

เหตุผล: โปรเจกต์เปิด "isolatedModules": true ไว้ = ตัวแปลงต้องอ่านไฟล์นี้ไฟล์เดียวแล้วตัดสินใจให้ได้ ว่าบรรทัดไหนต้องลบทิ้ง โดยห้ามเปิดไฟล์อื่นไปดู · บอกไว้ที่ไหนที่หนึ่งก็พอ · แต่ให้เขียนทั้งสองที่ตามสไลด์ เพราะมันกันอนาคต ไม่ใช่เพราะ compiler บังคับ

6 · Tailwind — utility แปลกลับเป็น CSS

ไม่มีอะไรใหม่ให้เรียนเลยสักบรรทัด — ฝั่งขวาคือ CSS จากบท 59–62 ทั้งหมด สิ่งเดียวที่ต้องเรียนคือวิธีย่อ · ทุกก้อนในตารางนี้ก็อปมาจากไฟล์ CSS ที่โปรเจกต์สร้างขึ้นจริง

utility ที่คุณพิมพ์CSS ที่ถูกสร้างขึ้นจริงแปลว่าเท่าไหร่
flexdisplay: flex—
flex-colflex-direction: column—
items-centeralign-items: center—
p-4padding: calc(var(--spacing) * 4)1rem = 16px
p-6padding: calc(var(--spacing) * 6)1.5rem = 24px
p-8padding: calc(var(--spacing) * 8)2rem = 32px
px-2.5padding-inline: calc(var(--spacing) * 2.5)0.625rem = 10px
py-1.5padding-block: calc(var(--spacing) * 1.5)0.375rem = 6px
mt-2margin-top: calc(var(--spacing) * 2)0.5rem = 8px
gap-4gap: calc(var(--spacing) * 4)1rem = 16px
borderborder-style: var(--tw-border-style); border-width: 1px1px
border-2border-style: var(--tw-border-style); border-width: 2px2px
border-transparentborder-color: #0000โปร่งใส
rounded-mdborder-radius: var(--radius-md)0.375rem = 6px
rounded-lgborder-radius: var(--radius-lg)0.5rem = 8px
text-basefont-size: var(--text-base) + line-height1rem · line-height 1.5
text-smfont-size: var(--text-sm)0.875rem = 14px
font-boldfont-weight: var(--font-weight-bold)700
text-red-500color: var(--color-red-500)#fb2c36 (v4 · ไม่ใช่ #ef4444 ของ v3)
text-gray-900color: var(--color-gray-900)#101828
text-green-700color: var(--color-green-700)#008138
duration-300transition-duration: .3s0.3 วินาที
text-[#aa3bff] (arbitrary value).text-\[\#aa3bff\] { color: #aa3bff; }ค่าที่คุณกำหนดเอง · \ คือ escape เพราะ # [ ( เป็นอักขระพิเศษใน selector
hover: · sm: · dark: (variant)กลายเป็น pseudo-class หรือ media query · hover: ถูกห่อไว้ใน @media (hover: hover)บนมือถือที่ไม่มีเมาส์ hover: ไม่ทำงานเลย — อย่าซ่อนข้อมูลสำคัญไว้ใต้มัน
⚠ p-4 ไม่ได้แปลว่า padding: 1rem ตรง ๆ — เลขหลังขีดคือตัวคูณ ไม่ใช่หน่วย
/* ก็อปจากไฟล์ CSS ที่โปรเจกต์สร้างจริง */
--spacing: .25rem;      ← 4 × 0.25rem = 1rem = 16px
--text-base: 1rem;      --text-sm: .875rem;
--radius-md: .375rem;   --radius-lg: .5rem;
--font-weight-bold: 700;
--color-red-500: #fb2c36;   --color-gray-900: #101828;

ประโยชน์จริงของการเขียนเป็น calc(): แก้ --spacing บรรทัดเดียว ระยะห่างทั้งเว็บขยับพร้อมกันหมด โดยที่กฎ .p-4 .p-6 .mt-2 ไม่ถูกแก้สักตัว เพราะมันอ้าง var(--spacing) อยู่แล้ว

v3 vs v4 — โปรเจกต์นี้เป็น v4 แต่อินเทอร์เน็ตยังเป็น v3

เรื่องTailwind v3 (ของเก่าที่ค้นเจอบ่อย)Tailwind v4 — ของจริงในโปรเจกต์นี้
เอาเข้าไฟล์ CSS@tailwind base; @tailwind components; @tailwind utilities; (3 บรรทัด)@import "tailwindcss"; (บรรทัดเดียว)
ไฟล์ตั้งค่าtailwind.config.jsไม่มีไฟล์นี้ — ls tailwind.config.* ได้ no matches found · ตั้งค่าด้วย @theme ใน CSS
PostCSS plugintailwindcss + autoprefixer@tailwindcss/postcss ตัวเดียว
บอกว่าสแกนไฟล์ไหนตั้ง content: [...]หาให้เองอัตโนมัติ ไม่ต้องตั้ง
ปรับ tokenแก้ theme.extend ใน JSเขียน CSS variable ใน @theme { }
สี red-500#ef4444#fb2c36
⚠ เอาไวยากรณ์ v3 มาใช้ในโปรเจกต์ v4 — มันไม่พัง มัน “ทำงานได้ครึ่งเดียว”
สิ่งที่วัดผลจริง
มี error ตอน compile ไหมไม่มี — dev server ไม่บ่นสักบรรทัด
หน้าเว็บเปิดได้ไหมได้ — HTTP 200
ไฟล์ CSS ที่ได้4,089 bytes ขณะที่หน้าปกติในโปรเจกต์เดียวกันได้ 21,648 bytes
มี .flex ไหม✅ มี
มี .p-4 · --spacing · preflight ไหม❌ ไม่มีเลยสักตัว (0 ครั้ง)

อาการที่เห็นคือ “ทำไม flex ติดแต่ padding ไม่ติด” · ถ้าเจอหน้าที่สไตล์มาบ้างไม่มาบ้าง ให้เช็คไวยากรณ์เวอร์ชันก่อนเช็คอย่างอื่น

❌ ชื่อคลาสจากตัวแปร — สไตล์หายเงียบ ๆ

const color = "blue";
<p className={`text-${color}-600`}>
# HTML — คลาสอยู่ครบ
class="text-blue-600"
# แต่ในไฟล์ CSS นับได้
text-blue-600  -> 0
# ไม่มี error · ไม่มี warning · HTTP 200

✅ เขียนชื่อเต็มไว้ทั้งหมด แล้วเลือก

const colors = {
	blue:  "text-blue-600",
	green: "text-green-700",
};
<p className={colors[color]}>
text-green-700 -> 1
.text-green-700 { color: var(--color-green-700); }

สาเหตุ: Tailwind อ่านไฟล์ของคุณเป็นตัวอักษร ตอน build ตอนนั้นโปรแกรมยังไม่ได้รัน ตัวแปร color ยังไม่มีค่า · สิ่งที่มันเห็นคือข้อความ text-${color}-600 ซึ่งไม่ใช่ชื่อ utility → ไม่สร้างกฎอะไรเลย · ลำดับคลาสใน className ไม่มีความหมาย — สิ่งที่ตัดสินคือลำดับกฎในไฟล์ CSS ซึ่ง Tailwind เรียงให้เอง โดยเอา variant อย่าง hover: ไว้ท้ายเสมอ

7 · ตาราง hooks ทั้งห้า

hookใช้ตอนไหนsignaturedependency array มีผลยังไง
useState ต้องการค่าที่ จำข้ามการวาด และ สั่งวาดใหม่ เมื่อเปลี่ยน · ตัวแปรธรรมดา (let count) ทำได้แค่อย่างแรก จอเลยค้าง const [state, setState] = useState(initial) ไม่มี · initial ถูกใช้เฉพาะรอบวาดแรก รอบต่อไป React ไม่สนใจค่านี้แล้ว
useEffect ไปยุ่งกับโลกนอก React — setInterval · addEventListener · WebSocket · subscribe · ไม่ใช่สำหรับคำนวณค่าจาก state ที่มีอยู่แล้ว useEffect(() => { … return cleanup; }, [deps]) ไม่ใส่อาร์เรย์ = หลังการวาดทุกครั้ง (กับดักอันดับหนึ่ง) · [] = ครั้งแรกครั้งเดียว (dev รันสองครั้ง) · [a, b] = ครั้งแรก + ทุกครั้งที่ a หรือ b เปลี่ยน
useMemo จำ ค่า ที่คำนวณได้ · ใช้เมื่อ (ก) คำนวณแพงจริง หรือ (ข) ผลลัพธ์เป็น object/array ที่ต้องส่งเป็น prop ให้ลูกที่ห่อ React.memo หรือใส่ใน dependency ของ useEffect const v = useMemo(() => calc(), [deps])
ฟังก์ชันที่ส่งให้รับอาร์กิวเมนต์ไม่ได้ ต้องเป็น () => เปล่า ๆ และต้อง return
dep เปลี่ยน = เรียกฟังก์ชันคำนวณใหม่ · ไม่เปลี่ยน = คืนค่าที่จำไว้ · เก็บได้ค่าเดียว คือค่าล่าสุด ไม่ใช่ตารางแคชย้อนหลัง
useCallback จำ ฟังก์ชัน (ตัวก้อนโค้ด) · ใช้ตอนส่งฟังก์ชันเป็น prop ให้ลูกที่ห่อ React.memo หรือต้องใส่ใน dependency array const fn = useCallback((arg) => { … }, [deps])
ฟังก์ชันที่ส่งให้รับอาร์กิวเมนต์ได้ · React ไม่เรียกมัน แค่เก็บก้อนไว้
เหมือน useMemo ทุกอย่าง · useCallback(fn, deps) = useMemo(() => fn, deps) เป๊ะ (รันพิสูจน์แล้ว)
useContext หยิบค่าที่วางไว้ชั้นบน โดยไม่ต้องส่ง prop ผ่านทีละชั้น (แก้ prop drilling) const v = useContext(MyContext) ไม่มี dependency array · component ที่เรียกมันจะวาดใหม่ทุกครั้งที่ค่าใน context เปลี่ยน — เปลี่ยนหรือไม่ ตัดสินด้วย Object.is

ทุกตัวเทียบ dependency ทีละช่องด้วย Object.is — ซึ่งบอกได้แค่ “เป็นก้อนเดียวกันไหม” ไม่ใช่ “หน้าตาเหมือนกันไหม” · เพราะฉะนั้น object · array · function ที่สร้างใหม่ทุกรอบ = ไม่เท่าเดิมทุกรอบ = รันใหม่ทุกรอบ · node -e 'console.log((()=>{}) === (()=>{}))' ได้ false

useState — สองท่าที่ต้องเลือกให้ถูก

❌ ส่งค่า — อ่าน count ของรอบเก่า

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
กด 1 ครั้ง -> Stale: 1     ไม่ใช่ 3
กด 2 ครั้ง -> Stale: 2     ไม่ใช่ 6

✅ ส่งฟังก์ชัน (updater) — รับค่าล่าสุดเข้ามา

setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
กด 1 ครั้ง -> Updater: 3
กด 2 ครั้ง -> Updater: 6

อย่าเหมาเปลี่ยนทุกที่เป็น (c) => c + 1 เพราะกลัวผิด — ใช้แบบส่งฟังก์ชันเมื่อค่าใหม่คำนวณจากค่าเก่า (บวก ลบ สลับ true/false ต่อท้าย array) · ถ้าค่าใหม่ไม่เกี่ยวกับค่าเก่าเลย เช่น setName('Robin') หรือ setCount(0) ตอนรีเซ็ต — เขียนส่งค่าตรง ๆ อ่านง่ายกว่ามาก

// อ่าน state ต่อจาก set ในบรรทัดถัดไป — console จริง กด 1 ครั้ง
[log] name right after setName = Taylor      ← ยังเป็นค่าเก่า
[log] render with name = Robin               ← ค่าใหม่มาถึงในรอบวาดถัดไป

useEffect — cleanup รันตอนไหนบ้าง

เหตุการณ์cleanup รันไหมผลรันที่ยืนยัน
dependency เปลี่ยนค่ารัน — ก่อน setup รอบใหม่เสมอย้ายห้องแชท: ❌ ตัดการเชื่อมต่อห้อง random มาก่อน ✅ เชื่อมต่อห้อง support ทุกครั้ง
component ถูกเอาออกจากหน้ารัน เป็นครั้งสุดท้ายลืม cleanup → ถอดออกแล้วยังมี 24 tick ใน 3 วินาที (timer ค้าง 8 ตัว) · มี cleanup → 0
โหมด dev เปิดหน้าครั้งแรกรัน (Strict Mode) — setup → cleanup → setupnext start (production) รันครั้งเดียว ไม่มี cleanup แทรกกลาง
state อื่นเปลี่ยน แต่ไม่อยู่ใน dependencyไม่รัน—

สไลด์หน้า 41 เขียนไม่ครบ — เขียนว่า cleanup รันตอน unmount อย่างเดียว · ประโยคที่ถูกกว่าคือ “cleanup รันก่อน setup รอบถัดไปเสมอ และรันอีกครั้งตอน component หายไปจากหน้า”

สิ่งที่อยากทำใช้ useEffect?ทำไม
ตั้ง setInterval / setTimeout✅ ใช่เป็นของนอก React ต้องมีคนสั่งเปิดและสั่งปิด
เปิด WebSocket / subscribe✅ ใช่เหตุผลเดียวกัน
addEventListener ที่ window✅ ใช่DOM คือโลกนอก React
คำนวณยอดรวมจาก state ที่มีอยู่แล้ว❌ ไม่คำนวณตอน render ไปเลย ไม่ต้องเก็บเป็น state ซ้ำ (derived state)
อัปเดต state ตอนผู้ใช้กดปุ่ม❌ ไม่ทำใน event handler ของปุ่มนั้น ตรงกว่าและอ่านง่ายกว่า
โหลดข้อมูลมาแสดงบนหน้า⚠️ ได้ แต่ไม่ใช่ทางแรกใน App Router ให้ดึงใน Server Component แทน จะไม่ต้องส่ง JS ไปเบราว์เซอร์เลย

useCallback / React.memo — ต้องมาคู่กัน ไม่งั้นได้ศูนย์

// พ่อมี onPing = useCallback(() => {}, []) → ก้อนเดิมตลอดแน่นอน · กด +1 สองครั้ง
[ChildPlain] วาดใหม่ครั้งที่ 3
[ChildPlain] วาดใหม่ครั้งที่ 4
[ChildPlain] วาดใหม่ครั้งที่ 5
[ChildPlain] วาดใหม่ครั้งที่ 6      ← ไม่ได้ห่อ memo → ไต่ต่อไปเรื่อย ๆ ทั้งที่ prop เป็นก้อนเดิมเป๊ะ

[ChildMemo]  วาดใหม่ครั้งที่ 2      ← ห่อ memo → หยุดตั้งแต่ตอนโหลดหน้า ไม่ขยับอีกเลย

useCallback ที่ลูกไม่ได้ห่อ React.memo คือโค้ดที่ไม่ทำอะไรเลย นอกจากทำให้ไฟล์ยาวขึ้น · และกลับกัน — ห่อ memo แต่ส่ง prop ที่เป็นก้อนใหม่ทุกครั้ง ก็ไม่เกิดผลเหมือนกัน

⚠ ลำดับที่ควรทำ — อย่าใส่ useMemo/useCallback ไว้ก่อนเพราะกลัวผิด
  1. เขียนแบบไม่ใส่ก่อนเสมอ — โค้ดที่อ่านง่ายชนะโค้ดที่ “อาจจะเร็วกว่า”
  2. เจอปัญหาจริง (หน้าสะดุด · พิมพ์แล้วช้า) แล้วค่อยเปิดดู
  3. วัดก่อนแก้ — React DevTools Profiler หรือ console.log นับรอบ
  4. รู้แล้วว่าใครคือตัวปัญหา ค่อยใส่ตรงจุดนั้น + React.memo
  5. วัดซ้ำว่าดีขึ้นจริง ถ้าไม่ดีขึ้นให้ถอดออก — ข้อที่คนข้ามมากที่สุด

เพราะ useMemo หนึ่งบรรทัดมีต้นทุนทุกครั้งที่วาด แม้ตอนที่คืนค่าเก่า: สร้างก้อนฟังก์ชันใหม่ · สร้างอาร์เรย์ dependency ใหม่ · เทียบทีละช่อง · จองที่เก็บค่าเก่าไว้ตลอดอายุ component · ถ้าข้างในเป็นแค่ a + b — สี่ข้อนี้แพงกว่าการบวกเลขเอง

✅ React Compiler ยังไม่เปิดในโปรเจกต์แบบมาตรฐาน — เช็คเองได้
// next.config.ts — ทั้งไฟล์ ไม่ได้ตัดอะไรออก
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

$ ls node_modules | grep -i react-compiler
(ไม่มีอะไรออกมา)

แปลว่า useMemo กับ useCallback ยังต้องเขียนเอง · วิธีเช็คในโปรเจกต์ของคุณ: เปิด next.config.ts หาคำว่า reactCompiler — ไม่เจอ = ยังไม่เปิด · อย่าเชื่อคำบอกเล่า ให้เปิดไฟล์ดู

8 · useContext — เช็คลิสต์ 6 ขั้น

createContext = ติดตั้งท่อ · <Provider value={…}> = เปิดวาล์วปล่อยน้ำเข้าท่อ · useContext = เปิดก๊อก · มันหยิบจาก Provider ตัวที่ใกล้ที่สุดที่อยู่เหนือจุดที่เรียก (ซ้อน Provider ได้ ตัวในสุดชนะ) และ Provider ที่ return ออกไปจาก component เดียวกับที่เรียก useContext จะไม่ถูกมองเห็น เพราะมันอยู่ “ใต้” ไม่ใช่ “เหนือ”

ขั้นทำอะไรถ้าลืมจะเจออะไร (ของจริง)
1 สร้างไฟล์ context แล้วใส่ 'use client' บรรทัดบนสุด You're importing a module that depends on `createContext` into a React Server Component module. — และพังทั้งเว็บ หน้าที่ไม่เกี่ยวกันเลยก็ 500 ตามไปด้วย
2 ประกาศ type ให้ชัด ค่าเริ่มต้นเป็น undefined
createContext<ThemeBox | undefined>(undefined)
ใส่ createContext("light") แบบสไลด์ → ลืมครอบ Provider แล้วได้ HTTP 200 พร้อมค่าปลอม theme = [light] ไม่มีใครเตือนเลย
3 เขียน custom hook ที่มี if (!ctx) throw new Error(…) แล้วให้ทุกคนเรียกผ่านตัวนี้ เรียก useContext ตรง ๆ แล้วได้ TypeError: Cannot destructure property 'mode' of … as it is undefined. ซึ่งไม่บอกเลยว่าลืมครอบ Provider
4 รับ children ใน Provider ให้ครบ — ({ children }: { children: ReactNode }) TS2304: Cannot find name 'children'. — สไลด์หน้า 54 พลาดข้อนี้
5 ถ้า value เป็น object / array / function ให้ห่อ useMemo / useCallback value={{ n }} = ก้อนใหม่ทุกรอบ → consumer วาดใหม่ทุกครั้งที่แม่วาด แม้ค่าไม่เปลี่ยนเลย (LiteralChild render #3…#6 ที่ n=1 ตลอด) · memo กันไม่อยู่ ถ้า component นั้นเป็น consumer ของ context ที่เปลี่ยน
ค่าที่เป็น string · number · boolean ไม่ต้องห่อ
6 ครอบ Provider ให้อยู่เหนือคนที่จะใช้ · หน้าเว็บที่ครอบไม่ต้องเป็น client ถอด Provider ออกแล้ว next build ล้มทั้งโปรเจกต์: Error occurred prerendering page … → ⨯ Next.js build worker exited with code: 1

Provider เขียนได้สองแบบ · และ context ไม่ใช่ตัวแทนของ Redux

<Ctx.Provider value="แบบเก่า">…</Ctx.Provider>    ← ยังใช้ได้ ไม่ต้องรีบไล่แก้
<Ctx value="แบบใหม่ React 19">…</Ctx>              ← สั้นกว่า · ทั้งคู่ให้ HTTP 200 · ไม่มี warning
ลักษณะของข้อมูลเหมาะกับ context ไหมเพราะอะไร
ธีม · ภาษา · ข้อมูลผู้ใช้ที่ล็อกอินอยู่✅ เหมาะมากเปลี่ยนนาน ๆ ที และเปลี่ยนทีก็ตั้งใจให้ทั้งหน้าเปลี่ยนตามอยู่แล้ว
ค่าที่ตั้งครั้งเดียวตอนเปิดแอป (base URL ของ API)✅ เหมาะมากไม่เปลี่ยนเลย จึงไม่มีปัญหา re-render ตั้งแต่แรก
ค่าในฟอร์มที่พิมพ์อยู่ · ค่าที่เปลี่ยนถี่มาก❌ ไม่เหมาะเปลี่ยนทุกตัวอักษร แล้วลากทั้ง subtree ให้วาดใหม่ตาม
state ทั้งแอปที่มีหลายสิบ field ปนกัน❌ ไม่เหมาะcontext เลือกไม่ได้ว่าใครสนใจ field ไหน — consumer ทุกตัววาดใหม่หมด
ข้อมูลจาก API ที่ต้อง cache · refetch · จัดการ loading❌ ไม่เหมาะcontext เก็บค่าได้อย่างเดียว งานนี้เป็นของ data-fetching library

วิธีคิดที่ใช้ตัดสินใจได้จริง: context แก้ปัญหา “ส่งค่ายังไง” ไม่ได้แก้ปัญหา “จัดการ state ยังไง” · ปัญหาคือ prop drilling → context ตอบโจทย์ · ปัญหาคือ state ซับซ้อน หลายคนแก้ หลายที่ฟัง อยากได้ devtools → นั่นคนละปัญหา

9 · custom hook — กฎ 4 ข้อ

custom hook คือฟังก์ชัน JavaScript ธรรมดาที่ชื่อขึ้นต้นด้วย use และข้างในเรียก hook ของ React · ไม่มี API พิเศษ ไม่ต้อง import อะไรเพิ่ม

กฎ (สไลด์หน้า 62)แปลเป็นภาษาคนใครเป็นคนจับได้
1 · Start with “use” ชื่อฟังก์ชันต้องขึ้นต้นด้วย use eslint ฟ้องตอนตรวจ (react-hooks/rules-of-hooks) · React ตอนรันไม่ดูชื่อเลย โค้ดยังรันได้ปกติ
2 · Call other hooks ข้างในต้องเรียก hook ของ React อย่างน้อยหนึ่งตัว ไม่มีใครฟ้อง — แต่ถ้าไม่ทำ คุณจะโดน eslint ฟ้องผิด ๆ ทีหลัง (called conditionally ทั้งที่โค้ดไม่ผิด) แล้วคุณจะเลิกเชื่อเครื่องมือ
3 · Top-level execution ห้ามเรียกใน if / loop / หลัง return ทั้ง eslint และ React ตอนรัน — React โยน error จนหน้าพัง
4 · Independent state ทุกที่ที่เรียก ได้ state ของตัวเอง ไม่แชร์กัน ไม่มีใครฟ้อง — เพราะมันไม่ใช่ error แต่เป็นวิธีที่มันทำงาน
⚠ สองข้อที่ “ไม่มีใครฟ้อง” คือสองข้อที่กินเวลาคุณมากที่สุด

ข้อ 1 กับ 3 มีเครื่องมือคอยตะโกนใส่คุณ ผิดปุ๊บรู้ปั๊บ · ข้อ 2 กับ 4 ผิดแล้วเงียบ โปรแกรมรันได้ ไม่มีสีแดง แต่พฤติกรรมไม่ใช่อย่างที่คุณคิด · เวลาไล่บั๊กเรื่อง hook ให้เริ่มสงสัยสองข้อนี้ก่อนเสมอ

// กฎข้อ 4 — Box ตัวเดียว เรียก useCounter(0) ข้างใน แล้ววางสองครั้ง
// กด A สามครั้ง · กด B หนึ่งครั้ง → ผลจริงบนจอ:
A = 3      B = 1        ← ไม่ใช่ 4 ทั้งคู่ และไม่ใช่ 3 ทั้งคู่

React ผูก state ไว้กับ "ตำแหน่งของ component ในต้นไม้" ไม่ได้ผูกไว้กับ "ฟังก์ชัน useCounter"
→ วาง Box กี่ตัว ก็ได้ช่องเก็บค่าเท่านั้นตัว

custom hook ไม่ใช่เครื่องมือแชร์ค่า มันคือเครื่องมือแชร์ วิธีทำ · อยากให้หลาย component เห็นค่าเดียวกัน ต้องยก state ขึ้นไปที่พ่อแม่ร่วมแล้วส่งลงมาเป็น prop หรือใช้ useContext · ท่ามาตรฐานคือใช้ทั้งคู่ — Context เก็บค่ากลาง แล้วห่อการอ่านค่านั้นไว้ใน custom hook ชื่อ useXxx

useDebounce — ตัวที่ได้ใช้บ่อยที่สุด

import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number): T {
	const [debounced, setDebounced] = useState(value);

	useEffect(() => {
		const id = setTimeout(() => setDebounced(value), delay);
		return () => clearTimeout(id);   // ← ทั้งหมดของ debounce
	}, [value, delay]);

	return debounced;
}

พิมพ์ s o l a r รวดเดียว → RAW: 5 บรรทัด แต่ SEARCH: 1 บรรทัด · ลบ clearTimeout ทิ้งเมื่อไหร่ กลายเป็น 5 ทันที

useToggle — โครงที่ใช้ซ้ำได้

import { useCallback, useState } from "react";

export function useToggle(initial = false) {
	const [isOpen, setIsOpen] = useState(initial);
	const open   = useCallback(() => setIsOpen(true), []);
	const close  = useCallback(() => setIsOpen(false), []);
	const toggle = useCallback(() => setIsOpen((v) => !v), []);
	return { isOpen, open, close, toggle };
}

คืน object ดีกว่าคืน array — คืน array แล้วคนเรียกต้องจำลำดับ (สลับ open กับ close ก็ไม่มีใครฟ้อง) · อยากได้แบบสไลด์ให้ใช้ return [isOpen, open, close, toggle] as const;

10 · ตารางแปล error ที่เจอบ่อย

Turbopack / Next.js — พังตอน next build

ข้อความแปลว่า · ทางแก้
You're importing a module that depends on `useState` into a React Server Component module. This API is only available in Client Components. ลืม 'use client' · เติมเป็นบรรทัดแรกสุดของไฟล์ที่พาธใต้ Turbopack build failed ชี้อยู่ (ไฟล์เดียวกับบรรทัดแรกของ Import trace — ไฟล์ที่ import hook เข้ามาจริง ไม่ใช่หน้าที่เรียกมัน) · ชื่อ hook ในข้อความบอกตัวที่ผิดจริง (useState · useEffect · createContext) — useMemo/useCallback ตัวเดียวไม่ทำให้พัง
The "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue. วาง 'use client' ไว้ใต้ import · ต้องเป็นบรรทัดแรกสุด เหนือ import ทุกบรรทัด
Error occurred prerendering page "/…" → ⨯ Next.js build worker exited with code: 1 หน้าหนึ่งหน้า throw ตอน prerender แล้วล้มทั้งโปรเจกต์ (เช่นลืมครอบ Provider) · อยากเก็บหน้านั้นไว้ดูให้ใส่ export const dynamic = 'force-dynamic' (จะขึ้น ƒ ในตาราง)
Another next dev server is already running. เปิด next dev ซ้อนกันสองตัวในโฟลเดอร์เดียวไม่ได้ · ปิดตัวเก่าหรือรันคนละโฟลเดอร์
Invalid project directory provided, no such directory: /…/lint สั่ง npx next lint ซึ่งไม่มีแล้วใน Next 16 · ใช้ npx eslint app components
Unknown command: "create-next-app@latest" ลืมเว้นวรรค · npm create next-app@latest หรือ npx create-next-app@latest

TypeScript — จาก npx tsc --noEmit

โค้ดข้อความ · แปลว่าอะไร
TS2503Cannot find namespace 'JSX'. — React 19 ย้าย JSX ออกจาก global · เติม import type { JSX } from "react"; หรือลบ : JSX.Element ทิ้งไปเลย (ส่วนใหญ่ทำอย่างหลัง)
TS2339Property 'priceTag' does not exist on type 'JSX.IntrinsicElements'. — ชื่อ component ขึ้นต้นด้วยตัวเล็ก · JSX.IntrinsicElements = รายชื่อแท็ก HTML ที่มีอยู่จริง · ข้อความไม่มีคำว่า “ตัวพิมพ์ใหญ่” อยู่เลยสักคำ
TS2339Property 'name' does not exist on type 'never'. — เขียน useState(null) ทำให้ TS สรุปว่าเป็น null ตลอดกาล · แก้เป็น useState<User | null>(null)
TS2322Property 'variant' does not exist on type 'IntrinsicAttributes & ButtonProps'. — ส่ง prop ที่ยังไม่ได้ประกาศ · เพิ่มด้วย { variant?: … } & React.ButtonHTMLAttributes<…>
TS2322Type '{ mode: string; … }' is not assignable to type 'string'. — createContext("light") ทำให้ช่องนี้เก็บได้แค่ string แต่ Provider ส่ง object ลงไป
TS2304Cannot find name 'children'. — Provider ไม่ได้รับ parameter แต่ไปใช้ {children} · เขียนเป็น ({ children }: { children: ReactNode }) =>
TS2307Cannot find module '@/components/Bases' or its corresponding type declarations. — ไม่มีไฟล์ index.ts ในโฟลเดอร์นั้น (หรือ path alias ไม่ตรง)
TS1205Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'. — ไม่มีคำว่า type เลยสักที่ในเส้นทางของ type · ใส่ที่บรรทัด import หรือ export ก็ได้ อย่างน้อยหนึ่งที่
TS2345Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>'. — ยัดสตริงใส่ setCount ที่ type ถูกเดามาจาก useState(0) · SetStateAction<number> = ตัวเลขตรง ๆ หรือ ฟังก์ชันที่รับเลขเก่าคืนเลขใหม่

React — ตอนรันในเบราว์เซอร์

ข้อความแปลว่า · ทางแก้
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. วนไม่รู้จบ · ไปมองสองที่เท่านั้น — ข้างใน effect มี setState ไหม และ dependency ตัวไหนเป็นของใหม่ทุกรอบ (object/array ที่สร้างสด) · React ไม่หยุดให้ มันแค่ log แล้วปล่อยวนต่อ (count ทะลุ 7,000 ใน 3 วินาที)
Hydration failed because the server rendered text didn't match the client.
+ 3:39:24 PM / - 3:39:23 PM
ค่าตั้งต้นถูกคำนวณสองที่คนละเวลา (new Date() · Math.random()) · เจอคำว่า hydration เมื่อไหร่ ให้ถามข้อเดียว: “ค่านี้เซิร์ฟเวอร์กับเบราว์เซอร์คำนวณแล้วได้เท่ากันไหม” · แก้โดยเริ่มจาก null แล้วให้ effect ใส่ค่าจริงหลังหน้าวาดเสร็จ
Minified React error #418 อาการเดียวกันข้างบน แต่บน production build — ไม่มี overlay อธิบายให้ เหลือลิงก์ให้ไปเปิดอ่านเอง · นี่คือเหตุผลที่ต้องไล่ error ให้หมดตั้งแต่ตอน dev
React has detected a change in the order of Hooks called by Page.
Error: Rendered more hooks than during the previous render.
เรียก hook ใน if / loop / หลัง return · ตารางที่มันพิมพ์มาให้คือของขวัญ — บอกตรง ๆ ว่ารอบก่อนเรียกไปกี่ตัว รอบนี้กี่ตัว
Each child in a list should have a unique "key" prop. ลืม key ใน .map() · next build ผ่านสบาย ๆ หน้าเว็บก็ดูปกติ — ตัวที่ฟ้องคือ console กับ linter · ใช้ key={item.id} อย่าใช้ index
TypeError: Cannot destructure property 'mode' of '…useContext(...)' as it is undefined. ลืมครอบ Provider แล้วเรียก useContext ตรง ๆ · ห่อด้วย custom hook ที่มี if (!ctx) throw แล้วข้อความจะบอกทางแก้เอง
Error [ERR_PACKAGE_PATH_NOT_EXPORTED] เขียน require('react/cjs/…') · package.json ของ React ไม่ได้เปิดพาธย่อยนี้ · ต้องเขียนเป็นพาธจริง ./node_modules/react/cjs/…

ESLint — npx eslint app components

กฎข้อความ · แปลว่าอะไร
react-hooks/exhaustive-deps
(warning)
React Hook useEffect has a missing dependency: 'userId'. Either include it or remove the dependency array — ชี้ตำแหน่งของอาร์เรย์ให้เลย ไม่ต้องเดา · เป็นแค่ warning → next build ยังผ่านและ deploy ได้ทั้งที่โค้ดผิด ให้ถือว่ามันคือ error
react-hooks/set-state-in-effect
(error)
Calling setState synchronously within an effect can trigger cascading renders — กฎใหม่ของ Next 16 + React 19 · สังเกตคำว่า synchronously: เรียก setState ใน callback ของ setInterval ไม่โดน · เรียกในบรรทัดแรกของ effect เลย โดน
react-hooks/rules-of-hooks
(error)
React Hook "useState" is called in function "getCounter" that is neither a React function component nor a custom React Hook function. … React Hook names must start with the word "use" — eslint อ่านชื่อ ไม่ได้วิเคราะห์โค้ด
react-hooks/rules-of-hooks
(error)
React Hook "useCounter" is called conditionally. React Hooks must be called in the exact same order in every component render — เรียกใน if · ถ้าตั้งชื่อไม่ขึ้นต้นด้วย use eslint จะเงียบสนิท แล้วไปพังตอนผู้ใช้กดปุ่มแทน
react/jsx-key
(error)
Missing "key" prop for element in iterator — eslint ของ create-next-app ถือว่าเรื่อง key เป็น error ไม่ใช่ warning
@typescript-eslint/no-unused-vars
(warning)
'id' is assigned a value but never used — ตัวนี้ไม่รู้เรื่อง React เลย แต่มันคือสัญญาณว่าคุณลืม clearInterval(id) พอดี

11 · ตารางกับดักรวมทั้งกลุ่ม

อาการที่เห็นสาเหตุทางแก้
สร้างโฟลเดอร์แล้ว แต่เปิด URL นั้นได้ 404 · ไม่มี error ใน terminal ไม่มีไฟล์ page.tsx ในโฟลเดอร์นั้น — หรือมีไฟล์ .tsx ชื่ออื่นอยู่ เพิ่ม page.tsx ที่ export default ฟังก์ชันคืน JSX · โฟลเดอร์ที่เป็นแค่ทางผ่านก็ได้ 404 เหมือนกัน
layout ไม่ครอบหน้าที่อยากให้ครอบ layout.tsx ครอบเฉพาะสิ่งที่อยู่ใต้โฟลเดอร์ของมัน · หน้าคนละสายไม่โดนครอบ เลื่อน layout ขึ้นไปให้อยู่เหนือทั้งสองสาย · “ครอบทุกหน้า” แปลว่า “ครอบทุกหน้าที่อยู่ใต้ตัวมัน”
เปิดหน้าแล้วเจอแต่เมนู เนื้อหาหายไปเฉย ๆ layout ลืมเขียน {children} ใส่กลับ · Next.js ไม่ฟ้องอะไรทั้งนั้น เป็นบั๊กที่หาไม่เจอง่าย ๆ
ค่าใน layout รีเซ็ตเป็น 0 ทั้งที่ “layout ไม่ถูกวาดใหม่” โหลดหน้าใหม่ทั้งหน้า (F5 / พิมพ์ URL แล้ว Enter) ไม่ใช่กดลิงก์ ใช้ <Link> ไม่ใช่ <a> ธรรมดา · ความ “ไม่ถูกวาดใหม่” มีผลแค่ตอนเดินอยู่ในเว็บ
ข้อความใน <Button>ตกลง</Button> หายไป ได้ปุ่มเปล่า แกะ children ออกมาแล้วลืมเอาไปวางใน JSX tsc ไม่ฟ้อง (แกะออกมาแล้วไม่ใช้ ไม่ผิด) · ต้อง curl ดู HTML จริง — “ไม่มี error” ไม่ได้แปลว่า “ถูก”
attribute แปลก ๆ โผล่บน DOM: <button variant="outlined"> ลืมดึง prop ของตัวเองออกก่อน {...restProps} destructure ออกมาก่อน · React 19 ไม่ error ไม่ warning เลยสักบรรทัด — จะไม่รู้ตัวจนเปิด DevTools
class="btn btn-contained undefined" — มีคำว่า undefined เป็นชื่อ class จริง ๆ ต่อ class ด้วย template literal `${a} ${className}` แล้วคนใช้ไม่ได้ส่ง className มา · template literal แปลง undefined เป็นข้อความ "undefined" [variantStyles, className].filter(Boolean).join(" ") · บั๊กนี้อยู่ในโค้ดสไลด์หน้า 31 จริง
แก้ component แล้วหน้าเว็บไม่เปลี่ยนตาม · ไม่มี error สักตัว · build ผ่าน ชื่อ component ขึ้นต้นด้วยตัวเล็กและตรงกับแท็ก HTML จริง (button · span · form · section) เปลี่ยนเป็นตัวใหญ่ · เช็คข้อนี้ก่อนไปไล่หา cache หรือ build system
flex ติดแต่ padding ไม่ติด · ไฟล์ CSS ผอมผิดปกติ ใช้ไวยากรณ์ Tailwind v3 (@tailwind base; …) ในโปรเจกต์ v4 @import "tailwindcss"; บรรทัดเดียว · ไม่มี error ให้เห็น หน้าเว็บก็ 200 ปกติ
หา tailwind.config.js ไม่เจอทั้งที่บทความบอกให้แก้ v4 ไม่มีไฟล์นั้นอีกแล้ว — ไม่ใช่เพราะคุณลงพลาด ตั้งค่าใน @theme { } ของ app/globals.css
คลาสอยู่ใน HTML ครบทุกตัว แต่สไตล์ไม่ติด ชื่อคลาสสร้างจากตัวแปร `text-${color}-600` · Tailwind สแกนไฟล์เป็นตัวอักษรตอน build ตอนนั้นตัวแปรยังไม่มีค่า เขียนชื่อเต็มไว้ทั้งหมดเป็น map แล้วค่อยเลือก · เช็คโดยค้นชื่อคลาสในไฟล์ CSS ว่าเจอกี่ครั้ง
กดปุ่มแล้วเลขบนจอไม่ขยับ แต่ console.log เห็นเลขเพิ่มขึ้นทุกครั้ง ใช้ let count แทน state · ตัวแปรเปลี่ยนจริงแต่ไม่มีใครสั่งให้ React วาดใหม่ useState · และรู้ว่าถ้า React วาดใหม่ด้วยเหตุอื่น let count = 0 จะทำงานอีกรอบ ค่าหายเกลี้ยง
setCount(count + 1) สามบรรทัดติด กดหนึ่งครั้งได้ +1 ไม่ใช่ +3 count ของรอบวาดนั้นเป็นค่านิ่งค่าเดียว ทั้งสามบรรทัดแปลว่า setCount(0 + 1) setCount((c) => c + 1) — ใช้เมื่อค่าใหม่คำนวณจากค่าเก่าเท่านั้น
console.log บรรทัดถัดจาก setName('Robin') ยังได้ Taylor ค่าถูกล็อกไว้ตั้งแต่ต้นรอบวาด · setName ไม่ได้แก้ตัวแปรตัวนั้น มันแค่ขอให้มีรอบวาดใหม่ อ่านค่าใหม่ในรอบวาดถัดไป · ไม่ใช่บั๊ก เป็นวิธีที่มันทำงาน
ตัวเลขพุ่งเป็นหลักหมื่นใน 3 วินาที · พัดลมโน้ตบุ๊กดัง useEffect ไม่ใส่ dependency array แล้วข้างในมี setState → วาดเสร็จ → effect รัน → setState → วาดใหม่ → วนไม่หยุด ใส่ [] หรือ dependency ที่ถูกต้อง · ตัวจุดชนวนคือการเปลี่ยน state ไม่ใช่การไม่ใส่อาร์เรย์ — effect ที่แค่ console.log ไม่ใส่อาร์เรย์ก็ไม่วน
ใส่ [users] แล้วก็ยังวนไม่หยุด dependency เป็น object/array ที่สร้างใหม่ทุกรอบ · Object.is ดูตัวตน ไม่ได้ดูเนื้อข้างใน ห่อ useMemo ให้เป็นก้อนเดิม หรือใส่เฉพาะ field ที่เป็นค่าตรง ๆ
ใส่ [] แล้ว effect ยังรันสองครั้ง Strict Mode ของโหมด dev — Next.js เปิดให้เอง · มันจงใจ mount → unmount → mount เพื่อตรวจ cleanup ของคุณ ไม่ต้องแก้ — เช็คว่า cleanup เขียนครบไหมแทน · next start รันครั้งเดียว · “รันสองครั้งแล้วพัง” แปลว่า effect ของคุณไม่ทนต่อการรันซ้ำ ซึ่งวันหนึ่งผู้ใช้จริงจะทำให้มันรันซ้ำจนได้
timer ยังเดินอยู่หลัง component หายจากหน้าจอไปแล้ว (24 tick ใน 3 วินาที) ลืม return cleanup · Strict Mode คูณสอง → ใส่ 4 ครั้ง ได้ timer ค้าง 8 ตัว return () => clearInterval(id); · ของที่ต้องมี cleanup เกือบ 100%: setInterval · setTimeout · addEventListener · WebSocket · subscribe()
Hydration failed … เวลาบนจอต่างกันหนึ่งวินาที useState(new Date()) — 'use client' ไม่ได้แปลว่า “รันแค่บนเบราว์เซอร์” เซิร์ฟเวอร์ยัง render หนึ่งรอบเพื่อทำ HTML ตั้งต้น เริ่มจาก null แล้วให้ effect ใส่ค่าจริง (ยอมเห็น --:--:-- วินาทีแรก) · ใน dev บั๊กนี้โผล่ 1 ใน 12 รอบ แต่บน production โผล่ทุกรอบ
ใส่ useCallback ทั่วไฟล์แล้วไม่เร็วขึ้นเลย ลูกไม่ได้ห่อ React.memo — ลูกธรรมดาวาดใหม่ทุกครั้งที่พ่อวาด ไม่สนใจ prop ต้องครบทั้งสองฝั่ง · ไม่งั้นได้ศูนย์ · next build ผ่านเงียบ ๆ ไม่มีใครเตือน
ตัวเลขไม่อัปเดต แล้วอยู่ดี ๆ ก็เด้งไปค่าที่ถูก (2 → 2 → 2 → 5 ข้าม 3 กับ 4) dependency array ขาดตัวหนึ่ง (useMemo(() => a + b, [a])) ใส่ให้ครบ · eslint บอกตำแหน่งให้แล้ว แต่เป็นแค่ warning และ build ยังผ่าน · ตอนคุณเปิดดู มันมักจะถูกไปแล้ว จึงสืบยากมาก
ธีมไม่เปลี่ยนตาม แต่หน้าเว็บ 200 ปกติ ไม่มี error createContext("light") ทำให้ guard if (!ctx) throw ไม่มีวันทำงาน เพราะ "light" เป็น truthy createContext<T | undefined>(undefined) — เปลี่ยนความผิดพลาดที่เงียบ ให้กลายเป็นความผิดพลาดที่ส่งเสียง
ลูกที่ห่อ memo ไว้ ก็ยังวาดใหม่ทุกครั้งที่แม่วาด <Ctx value={{ n }}> สร้าง object ก้อนใหม่ทุกรอบ · memo กันไม่อยู่ ถ้า component นั้นเป็น consumer ของ context ที่เปลี่ยน ห่อ value ด้วย useMemo · ค่าที่เป็น string/number/boolean ไม่ต้องห่อ
next dev พังทั้งเว็บ หน้าที่ไม่เกี่ยวกันเลยก็ได้ 500 compile error ของทั้งแอป ไม่ใช่ runtime error ของหน้าเดียว (เช่นลืม 'use client' ที่ไฟล์ context) อย่าไล่แก้หน้าที่กำลังเปิดอยู่ · อ่านบรรทัดแรกของ error กับ Import traces ว่าชี้ไปที่ไฟล์ไหน
ใส่ 'use client' ที่ page.tsx แล้ว “มันหายพัง” ทุกอย่างที่หน้านั้น import กลายเป็น client ตามไปด้วย — คุณเพิ่งลากทั้งหน้าไปเป็น client เพื่อแก้ปัญหาของไฟล์เดียว ใส่ที่ไฟล์ที่เรียก hook จริง แล้วปล่อยหน้าเป็น server ต่อไป · “มันหายพัง” ไม่ได้แปลว่า “แก้ถูกที่”
เรียก custom hook สองที่แล้วค่าไม่แชร์กัน (A = 3 · B = 1) ไม่ใช่บั๊ก — useState จองช่องไว้กับตำแหน่งของ component ในต้นไม้ ไม่ใช่กับตัวฟังก์ชัน อยากแชร์จริง ๆ ต้องยก state ขึ้นไปที่พ่อแม่ร่วม หรือใช้ useContext · custom hook แชร์วิธีทำ ไม่ได้แชร์ค่า
หน้าพังตอนกดปุ่ม ทั้งที่ตอนโหลดหน้าปกติดี เรียก hook ใน if — รอบแรกเงื่อนไขเป็นเท็จเลยไม่ถูกเรียก พอเป็นจริงจำนวน hook เปลี่ยน ย้ายขึ้นบนสุด หรือแยกเป็น component ย่อย · ถ้าตั้งชื่อไม่ขึ้นต้นด้วย use eslint จะเงียบสนิท แล้วไปพังตอนผู้ใช้กดปุ่มแทน
eslint ฟ้อง called conditionally ทั้งที่โค้ดไม่ผิดอะไรเลย ตั้งชื่อฟังก์ชันว่า useXxx ทั้งที่ข้างในไม่มี hook สักตัว · eslint ตัดสินจากชื่ออย่างเดียว เปลี่ยนชื่อเป็น formatBaht ไปเลย · error ปลอมเยอะ ๆ ทำให้คุณเลิกเชื่อเครื่องมือ ซึ่งแพงกว่า
รายการสลับกันมั่ว · พิมพ์ในช่องของการ์ดใบหนึ่ง ข้อความไปโผล่อีกใบ ลืม key หรือใช้ index เป็น key แล้วรายการถูกกรอง/เรียงใหม่ key={item.id} · build ผ่านและหน้าดูปกติ ตัวที่ฟ้องคือ console กับ npx eslint
ผู้ใช้เคาะ space bar หนึ่งครั้งในช่องค้นหา แล้วได้ผลครบทุกคน ไม่ได้ trim() · " " !== "" จึงเข้าไปกรองจริง แล้วบังเอิญทุกชื่อมีเว้นวรรคคั่น keyword.trim().toLocaleLowerCase() · ถ้าข้อมูลจริงมีคนชื่อเดี่ยวไม่มีนามสกุล คนนั้นจะหายไปโดยไม่มีอะไรบอก
ค้นไม่เจอแล้วหน้าจอว่างเปล่า ผู้ใช้ต้องเดาเองว่าเกิดอะไรขึ้น .map() บน array ว่างได้ “ไม่มีอะไร” · ไม่ error · โจทย์ไม่ได้บอกว่าต้องแสดงอะไร เติม empty state · ต่างกันสามบรรทัด แต่ต่างกันคนละเรื่องในสายตาผู้ใช้ · โจทย์ที่อ่านแล้ว “เข้าใจเลย” คือโจทย์ที่ยังไม่ได้ถาม
toLocaleLowerCase() ทำงานปกติบนเครื่องคุณ แต่พังบนเครื่องลูกค้า locale ตุรกี: 'ISABELLA'.toLocaleLowerCase('tr-TR') ได้ ısabella (ไม่มีจุด) → พิมพ์ i แล้วหา “Isabella” ไม่เจอ ตอนเปรียบเทียบให้ใช้ toLowerCase() ที่ให้ผลเหมือนกันทุกเครื่อง · toLocaleLowerCase() เหมาะกับตอนแสดงผลให้คนอ่าน
ก็อป customers จาก JSON ไป .sort() แล้วลำดับเปลี่ยนถาวร sort แก้ของเดิม · ของที่ import มาจาก JSON คือก้อนเดียวกันทั้งไฟล์ [...customers] ก่อนเสมอ · ไม่กลับมาเป็นลำดับเดิมจนกว่าจะรีเฟรชหน้า

💬 ถามได้เจอ error ที่ไม่มีในหน้านี้ ส่งมาสี่อย่าง — ไฟล์ component เต็ม ๆ · คำสั่งที่รัน · ข้อความที่ขึ้นทั้งก้อน (อย่าตัดให้เหลือบรรทัดเดียว ลำดับของ log คือเบาะแสหลัก) และเวอร์ชันของ next กับ react — เดี๋ยวรันซ้ำแล้วเพิ่มลงตาราง · ทุกบรรทัดในหน้านี้มาจากบท 69–78 ที่รันเจอจริงทั้งหมด

12 · อ้างอิง