🧰 ของที่ใช้ตรวจในบทนี้
next 16.3.4 (Turbopack)
react / react-dom 19.2.8
typescript 5
node v24.14.0
# คำสั่งที่ใช้:
npx tsc --noEmit # ไม่มี error (ไม่พิมพ์อะไรออกมาเลย)
npx eslint app components # เอาคำเตือน react-hooks มาโชว์
npx next build # ✓ Compiled successfully (เวลาที่โชว์ต่างกันทุกรอบ ไม่ต้องเทียบตัวเลข)
npx next dev -p 375 # แล้วกดปุ่มจริงในเบราว์เซอร์ อ่าน console
ทุกบรรทัด console.log · ทุกตัวเลข · และ error ทุกก้อนในบทนี้
ก็อปมาจากผลรันจริงบนเครื่องผม ไม่ได้พิมพ์จากความจำ
โค้ดทุกก้อนที่บอกว่าเป็น "ไฟล์" ผ่าน npx tsc --noEmit และ npx next build จริงทั้งหมด
1 · ก่อนอื่น: React วาดใหม่ทั้งตัว ทุกครั้งที่ state เปลี่ยน
นี่คือรากของทั้งบท ถ้าข้อนี้ไม่แน่น อีกสิบหัวข้อข้างล่างจะกลายเป็นการท่องจำทันที
ตอนคุณกด setCount(1) ในบท 73 สิ่งที่ React ทำคือ
เรียกฟังก์ชัน component ของคุณใหม่ทั้งฟังก์ชัน ตั้งแต่บรรทัดแรกถึงบรรทัดสุดท้าย
ไม่ได้แก้เฉพาะตัวเลขบนจอ · ไม่ได้ "อัปเดตแค่ส่วนที่เปลี่ยน" ในระดับโค้ด
— โค้ดในตัว component ถูกรันซ้ำหมด
แปลว่าทุกบรรทัดแบบนี้ ถูกทำใหม่ทุกครั้งที่วาด:
function TodoList({ todos, tab }) {
const visible = filterTodos(todos, tab); // ← คำนวณใหม่ทุกครั้ง
const handleClick = () => {}; // ← สร้างฟังก์ชันก้อนใหม่ทุกครั้ง
// ...
}
ปกติไม่เป็นไร เพราะงานพวกนี้เร็วมาก · แต่มันเริ่มเป็นเรื่องเมื่อ
filterTodosทำงานกับข้อมูลหลักหมื่นแถว → ช้าจนรู้สึกได้- ค่าที่คำนวณได้ถูกส่งเป็น prop ให้ลูก → ลูกเห็น "ของใหม่" ทุกครั้งเลยวาดใหม่ตาม
useMemo กับ useCallback เกิดมาเพื่อสองเคสนี้ เท่านั้น
2 · useMemo — จำ "ค่า" ที่คำนวณได้
สไลด์หน้า 44 เขียนไว้สั้นมากว่า "Cache the result of a calculation between re-renders" — แปลตรงตัวคือ เก็บผลลัพธ์ของการคำนวณไว้ใช้ข้ามการวาด
const cachedValue = useMemo(() => {
// calculate value
}, [dependencies])
สไลด์หน้า 45 แยกชิ้นส่วนไว้ 3 อย่าง ซึ่งตรงกับของจริงทุกตัว:
| ชิ้นส่วน | สไลด์เรียกว่า | พูดแบบภาษาคน |
|---|---|---|
cachedValue |
Value | ผลลัพธ์ที่ได้จากการเรียกฟังก์ชันคำนวณ — ไม่ใช่ตัวฟังก์ชัน |
() => {…} |
Calculation Function | ฟังก์ชันที่ไม่รับอาร์กิวเมนต์ และต้อง return ค่าออกมา |
[dependencies] |
Dependencies | ค่าทุกตัวในตัว component ที่ถูกใช้ข้างในการคำนวณ ต้องใส่ให้ครบ |
ตัวอย่างของสไลด์หน้า 45–46 คือ TodoList:
const TodoList = ({ todos, tab }) => {
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab);
}, [todos, tab]);
return (
<ul>
{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
};
แผนภาพหน้า 46 สรุปลำดับไว้ว่า Tab changed → Recalculate function → Re-render
อ่านให้ดี — มันบอกว่า "คำนวณใหม่" เกิดขึ้นเฉพาะตอน dependency เปลี่ยน
ส่วน "วาดใหม่" เกิดได้บ่อยกว่านั้นเยอะ
ของจริง: กดปุ่มที่ไม่เกี่ยวข้อง แล้วดูว่ามันคำนวณใหม่ไหม
ผมสร้างไฟล์นี้ในโปรเจกต์ Next.js จริง (components/MemoDemo.tsx) แล้วรัน:
'use client';
import { useMemo, useState } from 'react';
import { bump } from './counter'; // ตัวนับเล็ก ๆ ที่ผมเขียนเอง
function slowSum(n: number): number {
console.log(`[คำนวณ] รอบที่ ${bump('calc')} · n=${n}`);
let total = 0;
for (let i = 0; i < n; i += 1) total += i;
return total;
}
export default function MemoDemo() {
const [n, setN] = useState(1000);
const [tick, setTick] = useState(0);
const sum = useMemo(() => slowSum(n), [n]); // ผูกไว้กับ n ตัวเดียว
return (
<div>
<p>sum={sum} tick={tick}</p>
<button onClick={() => setN(v => v + 1000)}>เปลี่ยน n</button>
<button onClick={() => setTick(v => v + 1)}>tick +1</button>
</div>
);
}
เปิดหน้าเว็บ แล้วกดปุ่มตามลำดับนี้ · นี่คือ console จริง (ผมแทรกบรรทัด ##### ไว้เองเพื่อคั่นจังหวะ):
[คำนวณ] รอบที่ 1 · n=1000
[คำนวณ] รอบที่ 2 · n=1000
##### B · กด tick +1 (state ที่ไม่เกี่ยวกับ n) #####
##### C · กด เปลี่ยน n #####
[คำนวณ] รอบที่ 3 · n=2000
[คำนวณ] รอบที่ 4 · n=2000
ระหว่างบรรทัด B กับ C ว่างเปล่า — กด tick +1 แล้ว component ถูกวาดใหม่จริง
(ตัวเลข tick บนจอเปลี่ยนเป็น 1) แต่ slowSum ไม่ถูกเรียกเลย
เพราะ n ยังเป็น 1000 เท่าเดิม
พอกด "เปลี่ยน n" เท่านั้นแหละ มันถึงคำนวณใหม่ — และค่าที่ได้บนจอคือ sum=1999000 ซึ่งคือผลรวม 0 ถึง 1999 จริง ๆ
ทำไม log ขึ้นซ้ำสองบรรทัดทุกครั้ง? ไม่ใช่บั๊กของคุณ —
ตอน next dev Next เปิด React Strict Mode ไว้ให้
ซึ่งจงใจเรียกฟังก์ชัน component สองรอบ เพื่อจับโค้ดที่มีผลข้างเคียงแอบแฝง
ตอน next build ผมเห็น log ขึ้นรอบเดียว ในเทอร์มินัล
ดังนั้นเวลาอ่าน console ในโหมด dev ให้ดู "เพิ่มขึ้นหรือไม่เพิ่ม" ไม่ใช่ตัวเลขดิบ
useMemo คือกระดาษทดที่จดคำตอบข้อยากไว้
ครูให้ทำโจทย์เดิมซ้ำ ๆ ทั้งคาบ — ครั้งแรกคุณคิดจริง แล้วจดคำตอบไว้ข้างโจทย์
ครั้งต่อ ๆ ไปคุณแค่เหลือบดูว่า "โจทย์ยังเป็นตัวเลขชุดเดิมไหม" ถ้าใช่ก็ลอกคำตอบเดิมส่งเลย
"ตัวเลขชุดเดิมไหม" นั่นแหละคือ [dependencies]
ที่ไม่เหมือนกัน: กระดาษทดจริงเก็บคำตอบได้หลายข้อพร้อมกัน
แต่ useMemo เก็บได้ค่าเดียว คือค่าล่าสุดเท่านั้น
ถ้า dependency วิ่งกลับไปกลับมา n=1000 → n=2000 → n=1000
มันจะคำนวณใหม่ทั้งสามครั้ง ไม่ได้จำค่าของ 1000 ไว้รอ —
ใครคิดว่ามันเป็นตารางแคชย้อนหลังได้ จะประหลาดใจตอนโปรไฟล์
3 · useCallback — จำ "ฟังก์ชัน" ไม่ใช่ค่า
สไลด์หน้า 48 เขียนว่า "Caches a function definition between component re-renders"
สังเกตคำว่า definition — มันไม่เรียกฟังก์ชันของคุณ มันแค่เก็บก้อนฟังก์ชันนั้นไว้
const cachedFn = useCallback(() => {
// function
}, [dependencies])
ตัวอย่างของสไลด์หน้า 49–50:
const ProductPage = ({ productId, referrer }) => {
const handleSubmit = useCallback(
(orderDetails) => {
post(`/product/${productId}/buy`, { referrer, orderDetails });
},
[productId, referrer],
);
return <ShippingForm onSubmit={handleSubmit} />;
};
ต่างจาก useMemo ตรงที่ฟังก์ชันข้างในนี้ รับอาร์กิวเมนต์ได้
(orderDetails) เพราะมันคือฟังก์ชันที่จะถูกเรียกทีหลัง ตอนคนกดปุ่ม
ไม่ใช่ฟังก์ชันที่ React เรียกให้ตอนนี้
ทำไมต้อง "จำฟังก์ชัน" ในเมื่อโค้ดข้างในเหมือนเดิมทุกครั้ง
เพราะ JavaScript ตัดสินว่าฟังก์ชันสองก้อน "เท่ากัน" หรือไม่
จากว่ามันเป็นก้อนเดียวกันในหน่วยความจำหรือเปล่า ไม่ใช่จากเนื้อโค้ดข้างใน
เขียน () => {} สองครั้ง = ได้ของสองก้อนที่ไม่เท่ากัน แม้จะพิมพ์เหมือนกันเป๊ะ
ตัวช่วยก่อน — seeFn เก็บก้อนฟังก์ชันลง Set แล้วคืนจำนวนก้อนที่ไม่ซ้ำกัน
(Set ตัดของซ้ำด้วยกฎเดียวกับ === พอดี) · อยู่ใน components/counter.ts ไฟล์เดียวกับ bump:
const seen = new Map<string, Set<unknown>>();
export function seeFn(label: string, fn: unknown): number {
let s = seen.get(label);
if (!s) {
s = new Set<unknown>();
seen.set(label, s);
}
s.add(fn);
return s.size;
}
แล้วไฟล์ที่ใช้พิสูจน์จริง (components/IdentityDemo.tsx):
'use client';
import { useCallback, useMemo, useState } from 'react';
import { seeFn } from './counter';
export default function IdentityDemo() {
const [tick, setTick] = useState(0);
const plain = () => {}; // ไม่ห่ออะไรเลย
const cached = useCallback(() => {}, []); // ห่อด้วย useCallback
const viaMemo = useMemo(() => () => {}, []); // ห่อด้วย useMemo แต่คืนฟังก์ชัน
console.log(
`[Identity] tick=${tick}` +
` · plain มีก้อนที่ไม่ซ้ำกัน ${seeFn('plain', plain)} ก้อน` +
` · cached มีก้อนที่ไม่ซ้ำกัน ${seeFn('cached', cached)} ก้อน` +
` · viaMemo มีก้อนที่ไม่ซ้ำกัน ${seeFn('viaMemo', viaMemo)} ก้อน`,
);
return <button onClick={() => setTick(v => v + 1)}>identity tick +1</button>;
}
โหลดหน้าใหม่ แล้วกดปุ่มสองครั้ง · console จริง:
[Identity] tick=0 · plain มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
[Identity] tick=0 · plain มีก้อนที่ไม่ซ้ำกัน 2 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
[Identity] tick=1 · plain มีก้อนที่ไม่ซ้ำกัน 3 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
[Identity] tick=1 · plain มีก้อนที่ไม่ซ้ำกัน 4 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
[Identity] tick=2 · plain มีก้อนที่ไม่ซ้ำกัน 5 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
[Identity] tick=2 · plain มีก้อนที่ไม่ซ้ำกัน 6 ก้อน · cached มีก้อนที่ไม่ซ้ำกัน 1 ก้อน · viaMemo มีก้อนที่ไม่ซ้ำกัน 1 ก้อน
อ่านสามคอลัมน์นี้ให้ครบ:
plainเพิ่มขึ้นเรื่อย ๆ 1 → 6 — ทุกครั้งที่วาด ได้ฟังก์ชันก้อนใหม่จริง ๆcachedค้างที่ 1 ตลอด —useCallbackคืนก้อนเดิมให้ทุกครั้งviaMemoก็ค้างที่ 1 เหมือนกัน — พิสูจน์ว่าuseCallback(fn, deps)ทำงานเหมือนuseMemo(() => fn, deps)เป๊ะ (เอกสาร React เขียนไว้อย่างนั้น และผมรันแล้วเป็นอย่างนั้นจริง)
ลูกที่ห่อ React.memo ก็เหมือนยามหน้าตึกที่จำได้แค่ "เลขกล่องพัสดุ"
ทุกครั้งที่คุณเดินมาส่งของ ยามจะดูเลขกล่อง ถ้าเลขเดิมเขาก็โบกมือให้ผ่าน ไม่ต้องยกเข้าไปใหม่
ปัญหาคือคุณเปลี่ยนกล่องใหม่ทุกเช้า ทั้งที่ของข้างในชิ้นเดิม —
ยามเลยเห็นเลขไม่ซ้ำสักวัน แล้วสั่งยกเข้าไปใหม่ทุกวัน
useCallback คือการใช้กล่องใบเดิมซ้ำ
ที่ไม่เหมือนกัน: ยามจริงเปิดกล่องดูข้างในได้ถ้าอยากตรวจ
แต่ JavaScript เปิดดูไม่ได้เลย — มันไม่มีวิธีถามว่า "ฟังก์ชันสองก้อนนี้ทำงานเหมือนกันไหม"
ทำได้แค่ถามว่า "เป็นก้อนเดียวกันหรือเปล่า"
เพราะฉะนั้นอย่าคิดว่า React "ฉลาดพอจะรู้ว่าโค้ดเหมือนเดิม" — มันไม่รู้ และไม่มีวันรู้
- memoization
- เทคนิค "จำผลลัพธ์ของอินพุตชุดเดิมไว้ ไม่คำนวณซ้ำ" — เป็นที่มาของชื่อ
useMemoและReact.memo· ถาม senior ได้ว่า "ตรงนี้ควร memoize ไหม หรือปล่อยให้คำนวณใหม่ไปเลยก็ได้" คำถามนี้จะได้คำตอบตรงกว่าถามว่า "ทำยังไงให้เร็วขึ้น" - re-render (วาดใหม่)
- การที่ React เรียกฟังก์ชัน component ของคุณใหม่ทั้งฟังก์ชัน · คนละเรื่องกับการที่หน้าจอเปลี่ยนจริง — วาดใหม่แล้วผลออกมาเหมือนเดิม React ก็ไม่แตะ DOM เลย · พิมพ์บอก AI ได้ว่า "why does this component re-render when X changes" จะได้คำตอบที่ตรงจุดกว่าบอกว่า "หน้าเว็บกระตุก"
- referential equality (การเท่ากันโดยอ้างอิง)
- กฎที่ว่า object · array · function สองก้อนจะ "เท่ากัน" ก็ต่อเมื่อเป็นก้อนเดียวกัน
ไม่ใช่ "หน้าตาเหมือนกัน" · ผมรัน
node -e 'console.log((()=>{}) === (()=>{}))'ได้falseจริง · ถาม senior ได้ว่า "prop ตัวนี้เสีย referential equality ทุก render ใช่ไหม" — เป็นประโยคที่บอกอาการได้ครบในบรรทัดเดียว - dependency array
- อาร์เรย์ตัวหลังใน
useMemo/useCallback/useEffectที่บอก React ว่า "ถ้าค่าพวกนี้เปลี่ยน ให้ทำใหม่" · พิมพ์บอก AI ได้ว่า "check my dependency arrays for missing deps" แล้วเทียบกับที่eslintบอก อย่าเชื่อฝ่ายเดียว
4 · ตารางเทียบสองตัว — จุดที่ต่างกันมีจุดเดียว
useMemo | useCallback | |
|---|---|---|
| จำอะไร | ค่า (ผลลัพธ์ของการคำนวณ) | ฟังก์ชัน (ตัวก้อนโค้ด) |
| React เรียกฟังก์ชันที่ส่งให้ไหม | เรียก แล้วเก็บ ผลลัพธ์ ไว้ | ไม่เรียก เก็บ ตัวฟังก์ชัน ไว้เฉย ๆ |
| ฟังก์ชันที่ส่งให้รับอาร์กิวเมนต์ได้ไหม | ไม่ได้ — ต้องเป็น () => … เปล่า ๆ |
ได้ — (orderDetails) => … ก็ได้ |
| ได้อะไรกลับมา | ค่าที่ return ออกมา |
ฟังก์ชันก้อนเดิม |
| เคสที่ใช้บ่อยที่สุด | คำนวณหนัก · หรือทำ object/array ให้คงตัวเพื่อส่งเป็น prop | ส่งฟังก์ชันเป็น prop ให้ลูกที่ห่อ React.memo · หรือใส่ใน dependency ของ useEffect |
| เขียนแทนกันได้ไหม | useCallback(fn, deps) = useMemo(() => fn, deps) — ผมรันพิสูจน์แล้วในหัวข้อ 3 |
|
| dependency array | เหมือนกันทุกอย่าง · เทียบทีละช่องด้วยกฎ referential equality | |
useMemo
ส่งเข้าไป : () => filterTodos(todos, tab) ← ฟังก์ชันที่ "คืนค่า"
React ทำ : เรียกมัน แล้วเก็บผลไว้
ได้กลับมา : [{id:1}, {id:2}] ← ★ ค่า ★
useCallback
ส่งเข้าไป : (o) => post(url, o) ← ฟังก์ชันที่จะใช้ทีหลัง
React ทำ : ไม่เรียก · เก็บก้อนนี้ไว้เฉย ๆ
ได้กลับมา : (o) => post(url, o) ← ★ ฟังก์ชัน ★ ก้อนเดิมเป๊ะ
5 · กับดักที่ 1 — useCallback ที่ลูกไม่ได้ห่อ React.memo คือโค้ดที่ไม่ทำอะไรเลย
นี่คือความเข้าใจผิดที่แพงที่สุดของบทนี้ และเป็นเหตุผลที่โค้ดจำนวนมากเต็มไปด้วย
useCallback โดยไม่ได้เร็วขึ้นแม้แต่นิดเดียว
useCallback ทำได้อย่างเดียวคือทำให้ prop ที่ส่งลงไปเป็นก้อนเดิม
แต่ ลูกจะสนใจว่า prop เป็นก้อนเดิมหรือไม่ ก็ต่อเมื่อมันถูกห่อด้วย React.memo
ลูกธรรมดา ๆ ที่ไม่ได้ห่อ จะวาดใหม่ทุกครั้งที่พ่อวาดใหม่ ไม่ว่า prop จะเปลี่ยนหรือไม่
ไฟล์ที่ผมใช้พิสูจน์ (components/CallbackDemo.tsx) — ลูกสองตัว ต่างกันแค่ห่อกับไม่ห่อ:
'use client';
import { memo, useCallback, useState } from 'react';
import { bump } from './counter';
// ลูกคนที่ 1 — ไม่ห่ออะไรเลย
function ChildPlain({ onPing }: { onPing: () => void }) {
console.log(`[ChildPlain] วาดใหม่ครั้งที่ ${bump('plain')}`);
return <button onClick={onPing}>ChildPlain</button>;
}
// ลูกคนที่ 2 — ห่อด้วย React.memo
const ChildMemo = memo(function ChildMemoInner({ onPing }: { onPing: () => void }) {
console.log(`[ChildMemo] วาดใหม่ครั้งที่ ${bump('memo')}`);
return <button onClick={onPing}>ChildMemo</button>;
});
export default function CallbackDemo() {
const [count, setCount] = useState(0);
const onPing = useCallback(() => {}, []); // ← ก้อนเดิมตลอด
console.log(`[Parent] วาดใหม่ count=${count}`);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1 (count = {count})</button>
<ChildPlain onPing={onPing} />
<ChildMemo onPing={onPing} />
</div>
);
}
ก่อนดูผล — onPing ถูกห่อด้วย useCallback(…, []) แปลว่าเป็นก้อนเดิมตลอดแน่นอน
ถ้ากดปุ่ม +1 สองครั้ง (พ่อวาดใหม่ 2 รอบ)
ChildPlain กับ ChildMemo จะขึ้น log กี่บรรทัดต่อคน?
[Parent] วาดใหม่ count=0
[Parent] วาดใหม่ count=0
[ChildPlain] วาดใหม่ครั้งที่ 1
[ChildPlain] วาดใหม่ครั้งที่ 2
[ChildMemo] วาดใหม่ครั้งที่ 1
[ChildMemo] วาดใหม่ครั้งที่ 2
##### A · กด +1 ของ CallbackDemo สองครั้ง #####
[Parent] วาดใหม่ count=1
[Parent] วาดใหม่ count=1
[ChildPlain] วาดใหม่ครั้งที่ 3
[ChildPlain] วาดใหม่ครั้งที่ 4
[Parent] วาดใหม่ count=2
[Parent] วาดใหม่ count=2
[ChildPlain] วาดใหม่ครั้งที่ 5
[ChildPlain] วาดใหม่ครั้งที่ 6
ChildMemo ไม่โผล่อีกเลยหลังบรรทัด ##### —
ค้างอยู่ที่ครั้งที่ 2 ตั้งแต่ตอนโหลดหน้า
ส่วน ChildPlain ไต่ต่อไป 3 · 4 · 5 · 6 ทั้งที่ prop ที่มันได้รับเป็นก้อนเดิมเป๊ะ
แปลตรง ๆ ว่า: useCallback ตัวนั้นไม่ได้ช่วย ChildPlain เลยแม้แต่นิดเดียว
มันช่วยเฉพาะ ChildMemo
ถ้าในโค้ดคุณไม่มี React.memo อยู่เลยสักตัว แล้วคุณใส่ useCallback ไว้ทั่วไฟล์ —
คุณกำลังจ่ายค่าเขียนโค้ดเพิ่มโดยไม่ได้อะไรกลับมา
(บรรทัด ##### ผมพิมพ์ใส่ console เองเพื่อคั่นจังหวะ ไม่ได้มาจาก React)
สองอย่างนี้ต้องมาคู่กัน ไม่งั้นไม่เกิดผล
❌ ใส่ข้างเดียว = ไม่เกิดอะไร
const onPing = useCallback(fn, []);
// ...
<ChildPlain onPing={onPing} />
// ChildPlain ไม่ได้ห่อ memo
// → วาดใหม่ทุกครั้งอยู่ดี
✅ ใส่ครบทั้งคู่ = ลูกหยุดวาด
const onPing = useCallback(fn, []);
const ChildMemo = memo(Child);
// ...
<ChildMemo onPing={onPing} />
// → ข้ามการวาดได้จริง
และกลับกัน — ห่อ React.memo แต่ส่ง onPing ที่ไม่ได้ห่อ
useCallback ลงไป ก็ไม่เกิดผลเหมือนกัน เพราะ prop เป็นก้อนใหม่ทุกครั้ง
ต้องครบทั้งสองฝั่ง มิฉะนั้นได้ศูนย์
6 · กับดักที่ 2 — dependency array ผิด แล้วได้ค่าเก่าค้าง
กับดักข้อ 1 ทำให้โค้ดไม่เร็วขึ้น (น่ารำคาญแต่ไม่พัง)
ข้อนี้ทำให้ตัวเลขบนหน้าเว็บผิด ซึ่งแย่กว่ากันเยอะ และหายากกว่าเยอะ
'use client';
import { useMemo, useState } from 'react';
export default function StaleDemo() {
const [a, setA] = useState(1);
const [b, setB] = useState(1);
const bad = useMemo(() => a + b, [a]); // ใช้ b แต่ไม่ได้ใส่ b
const good = useMemo(() => a + b, [a, b]); // ใส่ครบ
return (
<div>
<p>a={a} b={b} bad={bad} good={good}</p>
<button onClick={() => setA(v => v + 1)}>a +1</button>
<button onClick={() => setB(v => v + 1)}>b +1</button>
</div>
);
}
เริ่มที่ a=1 b=1 · กด b +1 สองครั้ง แล้วค่อยกด a +1 หนึ่งครั้ง
bad กับ good จะแสดงเลขอะไรในแต่ละจังหวะ?
(ระวังจังหวะสุดท้าย มันไม่ตรงกับที่คนส่วนใหญ่เดา)
เริ่มต้น : a=1 b=1 bad=2 good=2
กด b +1 : a=1 b=2 bad=2 good=3
กด b +1 อีกครั้ง : a=1 b=3 bad=2 good=4
กด a +1 : a=2 b=3 bad=5 good=5
สองจังหวะกลาง bad ค้างอยู่ที่ 2 ทั้งที่ b ขยับไปถึง 3 แล้ว —
เพราะ [a] ไม่เปลี่ยน React จึงคืนค่าที่จำไว้ให้ ไม่เรียกฟังก์ชันคำนวณเลย
จังหวะสุดท้ายคือส่วนที่โหดที่สุด: พอ a ขยับ bad กระโดดจาก 2 ไป 5
ข้ามค่า 3 กับ 4 ไปเลย
บนหน้าเว็บจริง อาการนี้จะออกมาเป็น "ตัวเลขไม่อัปเดต แล้วอยู่ดี ๆ ก็เด้งไปค่าที่ถูก"
ซึ่งสืบยากมาก เพราะตอนคุณเปิดดู มันมักจะถูกไปแล้ว
ข่าวดี: มีเครื่องมือฟ้องให้ฟรี ๆ อยู่แล้ว
ผมรัน npx eslint กับไฟล์ทั้งโฟลเดอร์ ได้ผลนี้ตรง ๆ
(โปรเจกต์ที่ create-next-app สร้างมี eslint-config-next ติดมาให้แล้ว):
components/StaleDemo.tsx
9:37 warning React Hook useMemo has a missing dependency: 'b'. Either include it or remove the dependency array react-hooks/exhaustive-deps
✖ 1 problem (0 errors, 1 warning)
9:37 คือ บรรทัดที่ 9 ตัวอักษรที่ 37 ของไฟล์ข้างบน —
ชี้ตรงไปที่ [a] ไม่ใช่ที่ useMemo · มันบอกตำแหน่งของ
อาร์เรย์ที่ใส่ไม่ครบ ให้เลย ไม่ต้องเดา
มันเป็นแค่ warning ไม่ใช่ error
แปลว่า npx next build ยังผ่าน และเว็บยัง deploy ขึ้นได้ทั้งที่โค้ดผิด
กฎที่ผมใช้กับตัวเอง: เจอ react-hooks/exhaustive-deps เมื่อไหร่ ให้ถือว่ามันคือ error
ถ้าจะไม่ใส่ dependency ตัวนั้นจริง ๆ ต้องเขียนคอมเมนต์บอกเหตุผลไว้ให้คนต่อไปอ่าน
ไม่ใช่ปิดปาก eslint แล้วเดินหนี
7 · กับดักที่ 3 — ใส่ให้ทุกอย่างแล้วช้าลง
ความรู้สึกแรกของทุกคนหลังอ่านสองหัวข้อบนคือ "งั้นใส่มันทุกที่เลยดีกว่า ปลอดภัยไว้ก่อน"
อย่าทำ และเหตุผลไม่ใช่เรื่องสไตล์ แต่เป็นเรื่องต้นทุนที่มีอยู่จริง
useMemo หนึ่งบรรทัด ทำให้เกิดงานพวกนี้ ทุกครั้งที่วาด แม้ตอนที่มันคืนค่าเก่า:
- สร้างก้อนฟังก์ชันใหม่ที่ส่งเข้าไป — ข้อนี้ผมพิสูจน์แล้วในหัวข้อ 3
(คอลัมน์
plainที่ไต่จาก 1 ถึง 6) ·useMemoแค่ไม่เรียกมัน แต่มันถูกสร้างไปแล้ว - สร้างอาร์เรย์ dependency ใหม่ —
[a, b]คืออาร์เรย์ก้อนใหม่ทุกครั้ง - เทียบทีละช่องกับของรอบก่อน
- จองที่เก็บค่าเก่าไว้ตลอดอายุของ component
ถ้าการคำนวณข้างในเป็นแค่ a + b — งานสี่ข้อข้างบนแพงกว่าการบวกเลขเอง
แถมยังบวกภาระให้คนอ่านโค้ดอีกต่างหาก
ลำดับที่ควรทำ (เรียงจากบนลงล่าง อย่าข้ามขั้น)
- เขียนแบบไม่ใส่ก่อนเสมอ — โค้ดที่อ่านง่ายชนะโค้ดที่ "อาจจะเร็วกว่า"
- เจอปัญหาจริง (หน้าสะดุด · พิมพ์แล้วช้า) แล้วค่อยเปิดดู
- วัดก่อนแก้ — ใช้ React DevTools Profiler หรือ
console.logนับรอบแบบในบทนี้ - รู้แล้วว่าใครคือตัวปัญหา ค่อยใส่
useMemo/useCallback+React.memoตรงจุดนั้น - วัดซ้ำ ว่าดีขึ้นจริง ถ้าไม่ดีขึ้น ให้ถอดออก
ข้อ 5 คือข้อที่คนข้ามมากที่สุด · โค้ดที่ใส่ไว้แล้วไม่มีใครกล้าถอดออก คือหนี้ที่กองเพิ่มขึ้นทุกสัปดาห์
มีข้อยกเว้นข้อเดียวที่ควรใส่ตั้งแต่ต้น — เวลาค่านั้นเป็น
object หรือ array ที่ต้องส่งเป็น prop ให้ component ที่ห่อ React.memo
หรือต้องใส่ใน dependency array ของ useEffect
สองกรณีนี้ไม่ได้ทำเพื่อ "ความเร็ว" แต่ทำเพื่อ ความถูกต้อง —
ไม่ใส่แล้ว useEffect จะยิงทุกรอบไม่หยุด (บท 74 มีตัวอย่างลูปไม่รู้จบให้ดูแล้ว)
8 · เรื่องที่สไลด์ไม่ได้บอก — 'use client' กับ hook สองตัวนี้
ใน App Router ของ Next.js ทุกไฟล์ใน app/ เป็น Server Component โดยปริยาย
จนกว่าคุณจะเขียน 'use client' ไว้บรรทัดบนสุด
ตำราส่วนใหญ่จะสรุปสั้น ๆ ว่า "ใช้ hook = ต้องใส่ 'use client'"
— ผมลองจริงแล้วไม่ตรงทั้งหมด จึงเขียนตามที่รันได้จริง
ผลรันจริงที่ 1 — useMemo ล้วน ๆ ไม่ error
ไฟล์นี้ ไม่มี 'use client':
// app/t75/oops/page.tsx
import { useMemo } from 'react';
export default function Page() {
const total = useMemo(() => 1 + 1, []);
return <p>total = {total}</p>;
}
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:PORT/t75/oops
200
$ curl -s http://localhost:PORT/t75/oops | (ตัดแท็กออก)
Create Next App total = 2
$ npx next build
├ ○ /t75/oops ← ○ (Static) prerendered as static content
ผ่านฉลุย ไม่มี error สักตัว · เหตุผลอยู่ในตัว React เอง — ผมเปิดดูรายการที่ build ฝั่ง server ของ React 19.2.8 ส่งออกมา:
$ node -e "console.log(Object.keys(require('./node_modules/react/cjs/react.react-server.development.js')).sort().join(' '))"
Children Fragment Profiler StrictMode Suspense
__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE cache cacheSignal
captureOwnerStack cloneElement createElement createRef forwardRef isValidElement
lazy memo use useCallback useDebugValue useId useMemo version
ต้องเขียนพาธเป็น ./node_modules/react/cjs/… เท่านั้น ·
ถ้าเขียนสั้นว่า require('react/cjs/…') จะได้
Error [ERR_PACKAGE_PATH_NOT_EXPORTED] เพราะ package.json ของ React
ไม่ได้เปิดพาธย่อยนี้ให้เรียกผ่านชื่อแพ็กเกจ · (ผลข้างบนคือบรรทัดเดียวยาว ๆ
ผมตัดขึ้นบรรทัดใหม่เองให้อ่านง่าย ไม่ได้ตัดคำไหนทิ้ง)
useCallback กับ useMemo อยู่ในรายการ
ส่วน useState · useEffect ไม่อยู่
แต่ "ใช้ได้" ไม่เท่ากับ "ควรใช้"
Server Component ถูกวาดครั้งเดียวต่อหนึ่ง request แล้วจบ — ไม่มี re-render
ในเมื่อทั้ง useMemo และ useCallback มีไว้ "จำข้ามการวาดรอบถัดไป"
และมันไม่มีรอบถัดไป → ใส่ไปก็ไม่ได้อะไร นอกจากทำให้คนอ่านเข้าใจผิดว่าไฟล์นี้เป็น client
ผลรันจริงที่ 2 — พอเติม useState เข้าไป พังทันที
ไฟล์เดิม เติม useState หนึ่งบรรทัด ยังไม่ใส่ 'use client':
import { useMemo, useState } from 'react';
export default function Page() {
const [n] = useState(2);
const total = useMemo(() => n + 1, [n]);
return <p>total = {total}</p>;
}
เปิดหน้านั้นได้ HTTP 500 พร้อมข้อความนี้ (ก็อปมาทั้งดุ้น):
./app/t75/oops2/page.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 } from 'react';
| ^^^^^^^^
2 |
3 | export default function Page() {
4 | const [n] = useState(2);
Ecmascript file had an error
ดูตำแหน่งลูกศร ^^^^^^^^ ให้ดี — มันชี้ที่ useState
ไม่ได้ชี้ที่ useMemo ที่อยู่ข้าง ๆ กัน
นี่คือหลักฐานตรง ๆ ว่าตัวที่ผิดกฎคือ useState ตัวเดียว
สรุปให้ใช้ได้จริง: ในทางปฏิบัติคุณจะเจอ useMemo/useCallback
คู่กับ useState เสมอ (ไม่มี state ก็ไม่มีการวาดใหม่ให้ต้องจำ)
เพราะฉะนั้น ไฟล์ไหนมีสองตัวนี้ ให้ใส่ 'use client' ไปเลย —
ถูกในทางปฏิบัติเกือบร้อยเปอร์เซ็นต์
แต่ตอนนี้คุณรู้ด้วยว่าทำไม ไม่ใช่แค่ท่องว่า "hook ต้องใส่ use client"
9 · แล้ว React Compiler ที่เขาว่ากันล่ะ
คุณจะได้ยินคนพูดบ่อยมากว่า "React 19 มี compiler แล้ว ไม่ต้องเขียน useMemo เองอีกต่อไป"
ผมไม่เดา — เปิดไฟล์คอนฟิกของโปรเจกต์นี้มากางเลย:
// next.config.ts — ทั้งไฟล์ ไม่ได้ตัดอะไรออก
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
$ ls node_modules | grep -i react-compiler
(ไม่มีอะไรออกมา — ไม่ได้ติดตั้ง)
ในโปรเจกต์ที่ create-next-app สร้างมาแบบมาตรฐาน React Compiler ไม่ได้เปิดไว้
(ไม่มีบรรทัด reactCompiler ใน next.config.ts และไม่มีแพ็กเกจของมันใน node_modules)
แปลว่า useMemo กับ useCallback ยังจำเป็นอยู่ในโปรเจกต์นี้ ต้องเขียนเอง
วิธีเช็คในโปรเจกต์ของคุณเอง: เปิด next.config.ts หาคำว่า reactCompiler
ถ้าไม่เจอ = ยังไม่เปิด · อย่าเชื่อคำบอกเล่า ให้เปิดไฟล์ดู
- Server Component / Client Component
- ไฟล์ที่ React วาดบน server แล้วส่ง HTML มา (ค่าเริ่มต้นของ App Router)
กับไฟล์ที่ถูกส่งไปทำงานในเบราว์เซอร์ด้วย (ต้องมี
'use client') · ถาม senior ได้ว่า "component นี้ควรเป็น client หรือ server" แล้วเสริมว่า "มันต้องมี state ไหม" — เกณฑ์ตัดสินอยู่ตรงนั้น - stale value (ค่าเก่าค้าง)
- อาการที่หน้าจอโชว์ค่าที่คำนวณจากข้อมูลรอบก่อน เพราะ dependency ไม่ครบ · พิมพ์บอก AI ได้ว่า "I'm getting a stale value from this useMemo, check my deps" แล้วแปะโค้ดไปทั้งก้อน — คำนี้ทำให้คำถามแม่นขึ้นมาก
- React.memo
- ฟังก์ชันที่ห่อ component (คนละอย่างกับ
useMemoที่เป็น hook) เพื่อบอกว่า "ถ้า prop ทุกตัวเป็นก้อนเดิม ไม่ต้องวาดใหม่" · ถาม senior ได้ว่า "ตรงนี้ควรห่อ React.memo หรือย้าย state ลงไปให้ลึกกว่านี้แทน" — ทางเลือกที่สองมักจะดีกว่าและไม่มีใครนึกถึง - React Compiler
- ตัวแปลงโค้ดที่ใส่ memoization ให้อัตโนมัติตอน build เพื่อให้ไม่ต้องเขียนสองตัวนี้เอง · ถาม senior ได้ว่า "โปรเจกต์เราเปิด React Compiler ไว้หรือยัง" — ถ้ายัง ก็แปลว่ายังต้องเขียนมือ
10 · ภารกิจ — พิสูจน์ด้วยตัวนับ ไม่ใช่ด้วยความรู้สึก
🛠 ทำให้ลูกหยุดวาดใหม่ให้ได้ แล้วพิสูจน์ว่ามันหยุดจริง
สร้างโปรเจกต์ใหม่ด้วย npx create-next-app@latest
(หรือใช้ของบท 70 ที่มีอยู่แล้วก็ได้) แล้วทำตามลำดับ · ห้ามข้ามข้อ 4 กับ 6
-
สร้าง
components/RenderCount.tsx— ตัวนับที่ใช้ได้ทุกที่ ไม่ต้องลงแพ็กเกจอะไร:'use client'; const counts = new Map<string, number>(); export function bump(label: string): number { const n = (counts.get(label) ?? 0) + 1; counts.set(label, n); return n; } -
สร้างพ่อที่มี
useStateหนึ่งตัว (ปุ่ม +1) และลูกหนึ่งตัวที่รับ prop เป็นฟังก์ชัน
ใส่console.log(`ลูกวาดครั้งที่ ${bump('child')}`)ไว้บรรทัดแรกของลูก - กด +1 สามครั้ง จดเลขที่ได้ไว้ — นี่คือ "ก่อนแก้" ของคุณ (ในโหมด dev อย่าลืมว่า Strict Mode ทำให้เลขเดินทีละ 2)
-
ใส่
useCallbackอย่างเดียวก่อน แล้วกด +1 สามครั้งใหม่
คุณต้องได้เลขชุดเดิมเป๊ะ — ถ้าได้เลขน้อยลง แปลว่ามีอย่างอื่นเปลี่ยนไปด้วย ให้ย้อนกลับไปหา
ข้อนี้คือข้อที่สอนว่า "ใส่ข้างเดียวไม่พอ" ด้วยมือคุณเอง ไม่ใช่ด้วยคำพูดของผม -
ห่อลูกด้วย
memo()เพิ่ม แล้วกด +1 สามครั้ง — คราวนี้เลขต้องหยุดนิ่ง -
เขียนเทสพิสูจน์ — อย่าจบแค่ "เห็นแล้วว่านิ่ง" · เพิ่มโค้ดนี้ในลูก
แล้วตั้งค่าที่คาดหวังเอง:
แล้ว ทำให้มันขึ้นconst n = bump('child'); const want = 2; // จำนวนครั้งที่ "ควร" วาด (dev = 2 เพราะ Strict Mode) console.log(n <= want ? `PASS ลูกวาด ${n} ครั้ง (ไม่เกิน ${want})` : `FAIL ลูกวาด ${n} ครั้ง แต่ควรไม่เกิน ${want}`);FAILให้ได้หนึ่งครั้ง ด้วยการถอดmemo()ออกชั่วคราว
เทสที่ไม่เคยเห็นมันขึ้นFAILคือเทสที่คุณยังไม่รู้ว่ามันทำงานหรือเปล่า -
ปิดท้ายด้วยสองคำสั่งนี้ ต้องผ่านทั้งคู่:
npx tsc --noEmitnpx next build
แล้วรันnpx eslint app componentsดูว่ามีreact-hooks/exhaustive-depsโผล่ไหม
// ข้อ 4 — ใส่ useCallback อย่างเดียว ลูกไม่ได้ห่อ memo
##### A · กด +1 ของ CallbackDemo สองครั้ง #####
[ChildPlain] วาดใหม่ครั้งที่ 3
[ChildPlain] วาดใหม่ครั้งที่ 4
[ChildPlain] วาดใหม่ครั้งที่ 5
[ChildPlain] วาดใหม่ครั้งที่ 6 ← ยังเดินอยู่
// ข้อ 5 — ลูกอีกตัวที่ห่อ memo ในหน้าเดียวกัน กดปุ่มเดียวกัน
[ChildMemo] วาดใหม่ครั้งที่ 1
[ChildMemo] วาดใหม่ครั้งที่ 2 ← หยุดตั้งแต่ตอนโหลดหน้า ไม่ขยับอีกเลย
ถ้าของคุณข้อ 4 แล้วเลขหยุดเดิน ทั้งที่ยังไม่ได้ห่อ memo —
แปลว่าลูกของคุณอาจไม่ได้รับ prop จากพ่อจริง ๆ หรือพ่อไม่ได้วาดใหม่
ให้ console.log ที่พ่อด้วยหนึ่งบรรทัด แล้วดูว่าพ่อขยับไหม
— ไล่จากบนลงล่างเสมอ
ทดสอบความเข้าใจ
1. ข้อไหนอธิบาย "ความต่างระหว่าง useMemo กับ useCallback" ได้ตรงที่สุด?
ตอบตัวเลือกที่ 2 · ต่างกันแค่ "จำค่า" กับ "จำฟังก์ชัน" จบแค่นั้นจริง ๆ
ถึงขนาดที่ useCallback(fn, deps) เขียนเป็น useMemo(() => fn, deps) แล้วได้ผลเหมือนกันเป๊ะ —
ผมรันพิสูจน์ไว้ในหัวข้อ 3 แล้ว (คอลัมน์ viaMemo ค้างที่ 1 ก้อนเท่ากับ cached)
ตัวเลือกที่ 1 ผิดเพราะไม่เกี่ยวกับความหนักเบาของงาน ·
ตัวเลือกที่ 3 ผิดสองชั้น — ทั้งคู่ใช้ได้ทั้งสองฝั่ง (ผมเปิด export ของ React ฝั่ง server ให้ดูในหัวข้อ 8) ·
ตัวเลือกที่ 4 ผิดเพราะทั้งคู่รับ dependency array เหมือนกันทุกประการ
จุดที่ต้องจำ: useMemo จำค่า
useCallback จำฟังก์ชัน — ต่างกันแค่นี้จุดเดียว
2. พ่อมี const onPing = useCallback(() => {}, []) แล้วส่งให้ลูกที่ไม่ได้ห่อ React.memo · กดปุ่มให้พ่อวาดใหม่ 2 รอบ ลูกจะเป็นยังไง?
ตอบตัวเลือกที่ 3 · console จริงในหัวข้อ 5 แสดงชัดว่า ChildPlain ไต่ต่อไป
3 · 4 · 5 · 6 หลังกดปุ่มสองครั้ง ทั้งที่ onPing เป็นก้อนเดิมเป๊ะ
ส่วน ChildMemo ในหน้าเดียวกันหยุดนิ่งที่ 2
ตัวเลือกที่ 1 คือความเข้าใจผิดที่บทนี้ตั้งใจมาแก้โดยตรง ·
ตัวเลือกที่ 2 ผิดเพราะ React ไม่ได้จำอะไรให้เองเลยถ้าคุณไม่สั่ง ·
ตัวเลือกที่ 4 ผิด — ผมรัน npx next build แล้วผ่านเงียบ ๆ
ไม่มีใครเตือนสักคำ นี่แหละคือเหตุผลที่โค้ดแบบนี้อยู่รอดในโปรเจกต์จริงได้นานเป็นปี
จุดที่ต้องจำ: useCallback ที่ไม่มี React.memo
รออยู่ปลายทาง คือโค้ดที่ไม่ทำอะไรเลย
3. const bad = useMemo(() => a + b, [a]); โดยเริ่มที่ a=1 b=1 · กด b +1 สองครั้ง (จน b=3) แล้วกด a +1 หนึ่งครั้ง · bad จะเป็นเท่าไหร่ในแต่ละจังหวะ?
ตอบตัวเลือกที่ 1 · ค่าที่อ่านจากหน้าเว็บจริงคือ
bad=2 → bad=2 → bad=2 → bad=5
ตอน a เปลี่ยน React ถึงเรียกฟังก์ชันคำนวณใหม่ แล้วมันไปหยิบ b ตัวล่าสุด (3) มาบวก
จึงได้ 2 + 3 = 5 ทันที โดยไม่เคยผ่าน 3 กับ 4 เลย
ตัวเลือกที่ 2 ผิดเพราะนั่นคือพฤติกรรมของ good ที่ใส่ [a, b] ครบ —
ถ้า bad เดินตามนั้นได้ก็แปลว่า dependency array ไม่มีความหมายอะไรเลย ·
ตัวเลือกที่ 3 ผิดตรงที่มันไม่ได้ "ล็อกถาวร" — พอ [a] ขยับเมื่อไหร่ก็คำนวณใหม่ทันที ·
ตัวเลือกที่ 4 ผิด — ไม่มี error ตอนรันเลย มีแค่ warning ของ eslint
(react-hooks/exhaustive-deps) และ npx next build ก็ยังผ่าน
จุดที่ต้องจำ: dependency ขาดไม่ได้ทำให้โปรแกรมพัง แต่ทำให้ตัวเลขผิดเงียบ ๆ
แล้วเด้งกลับมาถูกเองตอนที่ dependency ตัวอื่นขยับ
4. ไฟล์ app/page.tsx ที่ไม่มี 'use client' แล้วเรียก useMemo อย่างเดียว (ไม่มี useState) — เกิดอะไรขึ้นตอนเปิดหน้านั้น?
ตอบตัวเลือกที่ 4 · ผมรันแล้วได้ HTTP 200 · หน้าแสดง total = 2 ปกติ
และ npx next build ยังทำเป็นหน้า static (○) ให้ด้วยซ้ำ
เพราะ build ฝั่ง server ของ React 19.2.8 มี useMemo · useCallback · useId
อยู่ในรายการ export จริง (ผมเปิดดูมาให้แล้วในหัวข้อ 8) — ที่ไม่มีคือ useState กับ useEffect
แต่ ไม่มีประโยชน์ เพราะ Server Component วาดครั้งเดียวต่อ request ไม่มีรอบถัดไปให้จำ ·
ตัวเลือกที่ 1 ผิดเพราะเหมารวมว่า "hook ทุกตัว" ·
ตัวเลือกที่ 2 ผิด — Next ไม่เคยเติม directive ให้ใครทั้งนั้น ·
ตัวเลือกที่ 3 ผิดเพราะ build ผ่านฉลุย · ตัวที่พังคือ useState เท่านั้น
และ error ก็ชี้ลูกศรไปที่คำว่า useState ตรง ๆ
จุดที่ต้องจำ: เส้นแบ่ง server/client ไม่ได้ขีดที่คำว่า "hook"
แต่ขีดที่ "ต้องจำอะไรข้ามการวาดไหม" — useState ต้อง ส่วน useMemo ไม่ต้อง
💬 ติดตรงไหนถามได้ — ถ้ารันตามในบทแล้วได้ผลไม่ตรงกับที่เขียนไว้
ให้ส่งมาสามอย่าง: โค้ดที่รัน · คำสั่งที่ใช้ ·
และ console ทั้งก้อน (อย่าตัดให้เหลือบรรทัดเดียว — ลำดับของ log คือเบาะแสหลัก)
และคำถามที่คุ้มที่สุดที่ถามได้กับบทนี้คือ
"ตรงนี้ควรใส่ useMemo หรือควรย้าย state ลงไปให้ลึกกว่านี้แทน" —
เพราะคำตอบที่ถูกบ่อยที่สุดคือ "ย้าย state" ไม่ใช่ "ใส่ memo"
อ้างอิง
- React —
useMemo— อ่านหัวข้อ Should you add useMemo everywhere? ซึ่งเป็นหัวข้อที่ทีม React เขียนเองเพื่อบอกว่า อย่า · แล้วเลื่อนต่อไปที่ Troubleshooting → My calculation runs twice on every re-render ถ้าอยากรู้ที่มาของ log ซ้ำสองบรรทัดที่เห็นในบทนี้ - React —
useCallback— อ่านหัวข้อ Skipping re-rendering of components ให้จบ เพราะมันเขียนตรง ๆ ว่าต้องใช้คู่กับmemoถึงจะเกิดผล — ตรงกับที่ console ในหัวข้อ 5 ของบทนี้แสดง · ในหน้าเดียวกันมีบรรทัดที่บอกว่าuseCallback(fn, deps)เท่ากับuseMemo(() => fn, deps) - React —
memo— อ่านหัวข้อ Caveats ที่เตือนว่ามันไม่ได้การันตีว่าจะไม่วาดใหม่ (React ยังมีสิทธิ์วาดใหม่ได้ในบางกรณี) — เป็นเหตุผลที่ห้ามใช้memoแทนการแก้ตรรกะที่ผิด - Next.js —
'use client'— ลิงก์นี้คือลิงก์เดียวกับที่โผล่ใน error ที่ผมก็อปมาในหัวข้อ 8 · อ่านย่อหน้าแรกที่อธิบายว่ามันขีดเส้นแบ่ง server/client ตรงไหน แล้วดูตัวอย่างที่บอกว่า directive ต้องอยู่บรรทัดบนสุดของไฟล์ เหนือimportทุกตัว - MDN —
Object.is()— อ่านตาราง Description · นี่คือฟังก์ชันที่ React ใช้เทียบ dependency ทีละช่อง ลองสังเกตว่ามันไม่มีทางบอกได้ว่า object สองก้อน "หน้าตาเหมือนกัน" — บอกได้แค่ "เป็นก้อนเดียวกัน"