🧰 ตรวจด้วยอะไร เวอร์ชันไหน — ทุกบรรทัดผลรันในหน้านี้มาจาก
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 ถ้าเวอร์ชันบนเครื่องคุณไม่ตรง ข้อความอาจต่างกันได้จริง — ให้เชื่อผลรันบนเครื่องคุณ ไม่ใช่ตัวหนังสือในหน้านี้
สารบัญ
- คำสั่งที่ใช้จริงทุกวัน
- โครงโฟลเดอร์ที่ได้มา — ไฟล์ไหนทำอะไร
- path ↔ URL (+ เทียบ Gin)
- Server Component vs Client Component
- component + props + children + spread
- Tailwind — utility แปลกลับเป็น CSS
- ตาราง hooks ทั้งห้า
- useContext — เช็คลิสต์ 6 ขั้น
- custom hook — กฎ 4 ข้อ
- ตารางแปล error ที่เจอบ่อย
- ตารางกับดักรวมทั้งกลุ่ม
- อ้างอิง
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 componentsnpm run lint |
สคริปต์ lint ที่ create-next-app ใส่มาให้คือ "eslint" เฉย ๆ ·
npx next lint ไม่มีแล้วใน Next 16 (ดูกล่องล่าง) |
| เช็คเวอร์ชัน | npx next --version · node --versionnode -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 นั่นแหละคือเหตุผลที่ใช้เป็นหลักฐานได้ |
$ 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.tsx | root 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)
| เรื่อง | Gin | Next.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 · กดจนเลขเป็น 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'
tsc --noEmitจับ error ตัวนี้ไม่ได้เลย — เส้นแบ่ง server/client เป็นกฎของ Next.js ไม่ใช่ของ TypeScript · VS Code จะไม่ขีดเส้นแดงให้ · “ไม่มีเส้นแดง” ไม่ได้แปลว่า “โค้ดใช้ได้” ในโปรเจกต์ Next.js- มันบ่นตอน
importไม่ใช่ตอนเรียก — แค่importทิ้งไว้เฉย ๆ ก็พังแล้ว - ติดครั้งเดียวคุ้มทั้งกิ่ง — ทุก component ที่ไฟล์นั้น import เข้ามากลายเป็น client ตามไปด้วย · เพราะงั้นอย่าแปะไว้บนสุดของต้นไม้ ให้แปะที่ component เล็กที่สุดที่ต้องใช้ hook จริง ๆ แล้ว JavaScript ที่ส่งไปเบราว์เซอร์จะน้อยลง
- ลืมใส่ที่ไฟล์ที่หน้าอื่นก็ 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 ที่ถูกสร้างขึ้นจริง | แปลว่าเท่าไหร่ |
|---|---|---|
flex | display: flex | — |
flex-col | flex-direction: column | — |
items-center | align-items: center | — |
p-4 | padding: calc(var(--spacing) * 4) | 1rem = 16px |
p-6 | padding: calc(var(--spacing) * 6) | 1.5rem = 24px |
p-8 | padding: calc(var(--spacing) * 8) | 2rem = 32px |
px-2.5 | padding-inline: calc(var(--spacing) * 2.5) | 0.625rem = 10px |
py-1.5 | padding-block: calc(var(--spacing) * 1.5) | 0.375rem = 6px |
mt-2 | margin-top: calc(var(--spacing) * 2) | 0.5rem = 8px |
gap-4 | gap: calc(var(--spacing) * 4) | 1rem = 16px |
border | border-style: var(--tw-border-style); border-width: 1px | 1px |
border-2 | border-style: var(--tw-border-style); border-width: 2px | 2px |
border-transparent | border-color: #0000 | โปร่งใส |
rounded-md | border-radius: var(--radius-md) | 0.375rem = 6px |
rounded-lg | border-radius: var(--radius-lg) | 0.5rem = 8px |
text-base | font-size: var(--text-base) + line-height | 1rem · line-height 1.5 |
text-sm | font-size: var(--text-sm) | 0.875rem = 14px |
font-bold | font-weight: var(--font-weight-bold) | 700 |
text-red-500 | color: var(--color-red-500) | #fb2c36 (v4 · ไม่ใช่ #ef4444 ของ v3) |
text-gray-900 | color: var(--color-gray-900) | #101828 |
text-green-700 | color: var(--color-green-700) | #008138 |
duration-300 | transition-duration: .3s | 0.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 plugin | tailwindcss + autoprefixer | @tailwindcss/postcss ตัวเดียว |
| บอกว่าสแกนไฟล์ไหน | ตั้ง content: [...] | หาให้เองอัตโนมัติ ไม่ต้องตั้ง |
| ปรับ token | แก้ theme.extend ใน JS | เขียน CSS variable ใน @theme { } |
สี red-500 | #ef4444 | #fb2c36 |
| สิ่งที่วัด | ผลจริง |
|---|---|
| มี 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 | ใช้ตอนไหน | signature | dependency 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 → setup | next 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 ไว้ก่อนเพราะกลัวผิด
- เขียนแบบไม่ใส่ก่อนเสมอ — โค้ดที่อ่านง่ายชนะโค้ดที่ “อาจจะเร็วกว่า”
- เจอปัญหาจริง (หน้าสะดุด · พิมพ์แล้วช้า) แล้วค่อยเปิดดู
- วัดก่อนแก้ — React DevTools Profiler หรือ
console.logนับรอบ - รู้แล้วว่าใครคือตัวปัญหา ค่อยใส่ตรงจุดนั้น +
React.memo - วัดซ้ำว่าดีขึ้นจริง ถ้าไม่ดีขึ้นให้ถอดออก — ข้อที่คนข้ามมากที่สุด
เพราะ useMemo หนึ่งบรรทัดมีต้นทุนทุกครั้งที่วาด แม้ตอนที่คืนค่าเก่า:
สร้างก้อนฟังก์ชันใหม่ · สร้างอาร์เรย์ dependency ใหม่ · เทียบทีละช่อง · จองที่เก็บค่าเก่าไว้ตลอดอายุ component ·
ถ้าข้างในเป็นแค่ a + b — สี่ข้อนี้แพงกว่าการบวกเลขเอง
// 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 ให้ชัด ค่าเริ่มต้นเป็น undefinedcreateContext<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
| โค้ด | ข้อความ · แปลว่าอะไร |
|---|---|
TS2503 | Cannot find namespace 'JSX'. — React 19 ย้าย JSX ออกจาก global ·
เติม import type { JSX } from "react"; หรือลบ : JSX.Element ทิ้งไปเลย (ส่วนใหญ่ทำอย่างหลัง) |
TS2339 | Property 'priceTag' does not exist on type 'JSX.IntrinsicElements'. —
ชื่อ component ขึ้นต้นด้วยตัวเล็ก · JSX.IntrinsicElements = รายชื่อแท็ก HTML ที่มีอยู่จริง ·
ข้อความไม่มีคำว่า “ตัวพิมพ์ใหญ่” อยู่เลยสักคำ |
TS2339 | Property 'name' does not exist on type 'never'. — เขียน useState(null)
ทำให้ TS สรุปว่าเป็น null ตลอดกาล · แก้เป็น useState<User | null>(null) |
TS2322 | Property 'variant' does not exist on type 'IntrinsicAttributes & ButtonProps'. —
ส่ง prop ที่ยังไม่ได้ประกาศ · เพิ่มด้วย { variant?: … } & React.ButtonHTMLAttributes<…> |
TS2322 | Type '{ mode: string; … }' is not assignable to type 'string'. —
createContext("light") ทำให้ช่องนี้เก็บได้แค่ string แต่ Provider ส่ง object ลงไป |
TS2304 | Cannot find name 'children'. — Provider ไม่ได้รับ parameter แต่ไปใช้ {children} ·
เขียนเป็น ({ children }: { children: ReactNode }) => |
TS2307 | Cannot find module '@/components/Bases' or its corresponding type declarations. —
ไม่มีไฟล์ index.ts ในโฟลเดอร์นั้น (หรือ path alias ไม่ตรง) |
TS1205 | Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'. —
ไม่มีคำว่า type เลยสักที่ในเส้นทางของ type · ใส่ที่บรรทัด import หรือ export ก็ได้ อย่างน้อยหนึ่งที่ |
TS2345 | Argument 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 · อ้างอิง
- Next.js —
'use client'directive — ลิงก์นี้ Next.js พิมพ์ให้เองในข้อความ error ตอน build · อ่านหัวข้อ How "use client" works ให้เห็นว่ามันแบ่งโปรเจกต์เป็นสองฝั่งยังไง และ Nesting Client Components within Server Components ที่อธิบายว่าทำไมต้องแปะให้ใกล้ใบ ไม่ใช่แปะไว้บนสุด - Next.js — Installation ·
Project Structure —
หน้าแรกอ่านแค่ Automatic installation จะเห็นว่าเอกสารทางการใช้
npx create-next-app@latest(ขีดกลาง) ทุกที่ · หน้าหลังดูตาราง Routing Files ที่ลิสต์ชื่อไฟล์พิเศษทั้งหมดที่ Next.js จองไว้ (page·layout·loading·error·not-found…) - Next.js —
layout.js— อ่านหัวข้อ Good to know ให้จบ มีข้อที่หัวข้อ 3 ของหน้านี้พิสูจน์ไว้แล้วสองข้อ และอีกข้อที่ยังไม่แตะ: layout รับsearchParamsไม่ได้ — จำไว้ เดี๋ยวจะไปงงตอนทำฟิลเตอร์ - React —
useState· State as a Snapshot · Queueing a Series of State Updates — อ่าน Caveats ของ set function ที่เขียนตรง ๆ ว่ามันไม่เปลี่ยนค่าในโค้ดที่กำลังรันอยู่ · แล้วดูตารางไล่คิวทีละบรรทัด จะเห็นชัดว่าทำไมsetNumber(number + 1)สามครั้งได้ +1 แต่setNumber(n => n + 1)ได้ +3 - React —
useEffect· Synchronizing with Effects · You Might Not Need an Effect — หน้าแรกอ่าน Parameters ตรงที่บอกว่า React เทียบ dependency ด้วยObject.is· หน้าที่สองอ่าน How to handle the Effect firing twice in development? · หน้าที่สาม ESLint แปะลิงก์มาให้เองในข้อความ error — เริ่มที่ Updating state based on props or state - React —
useMemo· useCallback · memo — อ่าน Should you add useMemo everywhere? ซึ่งทีม React เขียนเองเพื่อบอกว่าอย่า · ในหน้าuseCallbackมีบรรทัดที่บอกว่าuseCallback(fn, deps)เท่ากับuseMemo(() => fn, deps)· ในหน้าmemoอ่าน Caveats ข้อที่บอกว่า memo ไม่กัน re-render ที่เกิดจาก context - React —
useContext· createContext · Passing Data Deeply with Context — อ่าน Caveats ที่บอกว่า Provider ที่ return จาก component เดียวกันจะไม่ถูกมองเห็น · Optimizing re-renders when passing objects and functions คือหัวข้อ 8 ขั้นที่ 5 ของหน้านี้ · และ Before you use context ที่แนะนำให้ลองสองทางก่อนเสมอ - React — Reusing Logic with Custom Hooks · Rules of Hooks — อ่านหัวข้อ Custom Hooks share stateful logic, not state itself ซึ่งตอบกฎข้อ 4 โดยตรง · หน้าหลังคือลิงก์เดียวกับที่ React พิมพ์ใน error ตอนหน้าพัง
- React — Your First Component ·
Passing Props to a Component ·
Rendering Lists —
ดูกล่อง Pitfall เรื่องชื่อ component ต้องขึ้นต้นด้วยตัวใหญ่ · Forwarding props with the JSX spread syntax
(คือ
{...restProps}เป๊ะ ๆ · คู่มือเตือนเองว่า “ใช้ให้เท่าที่จำเป็น”) · และ Why does React need keys? - React 19 Upgrade Guide —
อ่านหัวข้อ The JSX namespace in TypeScript · เป็นเอกสารต้นทางของ
TS2503 - Tailwind CSS — Detecting classes in source files ·
Theme variables ·
Upgrade guide (v3 → v4) —
หน้าแรกอ่าน Dynamic class names ซึ่งเป็นเอกสารทางการของกับดักชื่อคลาสจากตัวแปร ·
หน้าที่สองดูตาราง Theme variable namespaces ที่ใช้แทน
tailwind.config.jsของ v3 · หน้าที่สามอ่าน CSS-first configuration ตอนที่ค้นเจอบทความเก่าแล้วไม่แน่ใจว่าต้องแปลงอะไร - MDN —
Object.is()· String.prototype.toLocaleLowerCase() — อันแรกคือฟังก์ชันที่ React ใช้เทียบ dependency ทีละช่อง — สังเกตว่ามันไม่มีทางบอกได้ว่า object สองก้อน “หน้าตาเหมือนกัน” · อันที่สองดูตัวอย่าง'İ'กับ localetrที่ทำให้ผลต่างจากtoLowerCase() - npm Docs —
npm init— อ่านย่อหน้า Description ที่บอกว่าnpm createเป็นชื่อเล่นของnpm init· ต้นตอทั้งหมดของกับดักในหัวข้อ 1 · อ่านแล้วจะเดาได้เองว่าทำไมnpm create vite@latestถึงต้องเว้นวรรคเหมือนกัน - Cheat sheet — TypeScript (บท 63–68) —
typevsinterface·anyvsunknown·map/filter/reduce· ตารางแปล error ของtsc· เปิดคู่กับหน้านี้เวลาเจอTSxxxxที่ไม่ได้อยู่ในหัวข้อ 10