useCallback คือฮุก React ที่ส่งคืนเวอร์ชันที่ถูกจดจำของฟังก์ชันซึ่งจะไม่เปลี่ยนแปลงระหว่างการเรนเดอร์จนกว่าค่าที่ขึ้นต่อ (dependencies) จะเปลี่ยนแปลง ต่างจากการประกาศฟังก์ชันปกติภายในคอมโพเนนต์ (ซึ่งสร้างฟังก์ชันใหม่ทุกครั้งที่เรนเดอร์) useCallback จะทำให้การอ้างอิงฟังก์ชันมีเสถียรภาพ ป้องกันการเรนเดอร์ซ้ำที่ไม่จำเป็นของคอมโพเนนต์ลูกที่ถูกปรับแต่งด้วย React.memo ตาม React Documentation (2025) useCallback จะมีประโยชน์ก็ต่อเมื่อใช้กับ React.memo หรือฮุกที่ขึ้นอยู่กับการอ้างอิงที่เสถียร
ประเด็นสำคัญ
useCallback คือฮุกที่ถูกเพิ่มใน React 16.8 ซึ่งจดจำฟังก์ชัน: ส่งคืนการอ้างอิงเดียวกันจนกว่าค่าที่ขึ้นต่อจะเปลี่ยนแปลง หากไม่มี useCallback ทุกการประกาศฟังก์ชันภายในคอมโพเนนต์จะสร้างออบเจกต์ฟังก์ชันใหม่ทุกครั้งที่เรนเดอร์ สำหรับค่าพื้นฐานแล้วสิ่งนี้ไม่สังเกตเห็น แต่เมื่อส่ง callback เหล่านี้ไปยังคอมโพเนนต์ลูกที่ถูกปรับแต่งด้วย React.memo ทุกการอ้างอิงใหม่จะทำให้เกิดการเรนเดอร์ซ้ำของคอมโพเนนต์ลูก
ในเชิงวากยสัมพันธ์ useCallback เทียบเท่ากับ useMemo สำหรับฟังก์ชัน: useCallback(fn, deps) เป็นตัวย่อของ useMemo(() => fn, deps) React จะเก็บฟังก์ชันที่ถูกจดจำไว้ในพื้นที่จัดเก็บภายในของโหนด fiber และเปรียบเทียบค่าที่ขึ้นต่อทุกครั้งที่เรนเดอร์ หากค่าที่ขึ้นต่อไม่เปลี่ยนแปลง (Object.is สำหรับแต่ละรายการ) ฟังก์ชันก่อนหน้าจะถูกส่งคืน
ตาม React Documentation — useCallback (2025) คุณไม่ควรครอบทุกฟังก์ชันด้วย useCallback ฮุกมีต้นทุนของมัน: การเรียกฮุก การเปรียบเทียบค่าที่ขึ้นต่อ และการจัดสรรหน่วยความจำสำหรับอาร์เรย์ของค่าที่ขึ้นต่อ หากคอมโพเนนต์เรียบง่ายและไม่มีโครงสร้างต้นไม้ลึกกับ React.memo useCallback จะทำให้แอปพลิเคชันช้าลงเท่านั้น การปรับแต่งควร วัดผลได้ ไม่ใช่โดยสัญชาตญาณ
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// การอ้างอิงที่เสถียร — ฟังก์ชันเดียวกันจนกว่า deps จะเปลี่ยน
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
การจดจำใน useCallback ขึ้นอยู่กับการแคชผลลัพธ์ของการเรียกฟังก์ชัน React จะเก็บ closure ที่ถูกสร้างขึ้นในขณะที่เรนเดอร์ครั้งแรกและส่งคืนในการเรนเดอร์ครั้งต่อๆ ไปตราบใดที่ค่าที่ขึ้นต่อยังคงไม่เปลี่ยนแปลง ภายในโหนด fiber ทุกการเรียก useCallback จะสร้างโหนดในรายการเชื่อมโยงของฮุก ซึ่งเก็บค่าที่ขึ้นต่อก่อนหน้าและค่าที่ถูกจดจำ
การเปรียบเทียบค่าที่ขึ้นต่อจะดำเนินการอย่างเคร่งครัดผ่าน Object.is — การเปรียบเทียบระดับพื้นผิวโดยไม่ตรวจสอบออบเจกต์หรืออาร์เรย์แบบลึก หากค่าที่ขึ้นต่อเป็นออบเจกต์หรืออาร์เรย์ การอ้างอิงใหม่ทุกครั้งที่เรนเดอร์จะถือว่าเป็นการเปลี่ยนแปลง ดังนั้น อาร์เรย์ของค่าที่ขึ้นต่อควรประกอบด้วยค่าพื้นฐานหรือการอ้างอิงที่เสถียร (เช่น จาก useRef หรือ useMemo)
ตาม React Core Team — Optimization Guide (2024) ต้นทุนของการจดจำประกอบด้วยสามส่วน: การจัดสรรอาร์เรย์ของค่าที่ขึ้นต่อในทุกการเรนเดอร์ การวนซ้ำและเปรียบเทียบองค์ประกอบผ่าน Object.is และค่าใช้จ่ายในการเก็บขยะที่อาจเกิดขึ้นเมื่อสร้างใหม่ สำหรับคอมโพเนนต์ที่มี useCallback หลายร้อยตัว สิ่งนี้อาจเห็นได้ชัด — ดังนั้น การเลือกใช้ ฮุกอย่างมีวิจารณญาณจึงสำคัญ
| สถานการณ์ | ไม่มี useCallback | มี useCallback |
|---|---|---|
| การสร้างฟังก์ชัน | ใหม่ทุกครั้งที่เรนเดอร์ | เหมือนเดิมกับ deps ที่เสถียร |
| ส่งต่อไปยัง React.memo | ลูกเรนเดอร์ซ้ำ | ลูกไม่เรนเดอร์ซ้ำ |
| ในอาร์เรย์ของ useEffect | เอฟเฟกต์เริ่มต้นใหม่ | เอฟเฟกต์เสถียร |
| ค่าใช้จ่าย | น้อยที่สุด | การเปรียบเทียบค่าที่ขึ้นต่อ + หน่วยความจำ |
มีความเชื่อผิดๆ อย่างแพร่หลายว่า useCallback ช่วยปรับปรุงประสิทธิภาพโดยอัตโนมัติ ในความเป็นจริง ตามลำพัง (ไม่มี React.memo) useCallback กลับทำให้แอปพลิเคชันช้าลงเล็กน้อยเนื่องจากต้นทุนการเปรียบเทียบค่าที่ขึ้นต่อ ฮุกจะให้ประโยชน์จริงๆ ในสามสถานการณ์เท่านั้น: ป้องกันการเรนเดอร์ซ้ำของคอมโพเนนต์ React.memo, ทำให้ callback ใน useEffect เสถียร และส่ง callback ไปยังฮุกแบบกำหนดเองที่ขึ้นอยู่กับความเท่ากันของการอ้างอิง
กฎง่ายๆ คือ: จนกว่าคุณจะตรวจพบปัญหาด้านประสิทธิภาพผ่าน React DevTools Profiler — อย่าใช้ useCallback ทีม React ย้ำหลายครั้งว่าการปรับแต่งก่อนเวลาอันควรเป็นต้นเหตุของปัญหาทั้งปวง ขั้นแรกเขียนโค้ดที่สะอาดโดยไม่มีการจดจำ วัดผล ค้นหาจุดคอขวดในโปรไฟเลอร์ แล้วค่อยเพิ่ม useCallback ในที่ที่ จำเป็นจริงๆ
// การปรับแต่งที่วัดผลได้: Child ถูกครอบด้วย React.memo
const Child = React.memo(({ onClick }) => {
console.log('Child เรนเดอร์ซ้ำแล้ว');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('ถูกคลิก');
}, []);
return <Child onClick={handleClick} />;
}
ตาม Dan Abramov — Before You memo() (2024) กว่า 90% ของการใช้ useCallback ในโปรเจกต์โอเพนซอร์สนั้นซ้ำซ้อน นักพัฒนาครอบทุกฟังก์ชัน "เผื่อไว้" โดยไม่วัดผล ทางเลือก: หากคอมโพเนนต์ลูกหนักและการเรนเดอร์ซ้ำมีต้นทุนสูง — React.memo + useCallback ก็สมเหตุสมผล หากคอมโพเนนต์ลูกเบา — การเรนเดอร์ซ้ำถูกกว่าการเปรียบเทียบค่าที่ขึ้นต่อ
สถานการณ์แรกคือ React.memo หากคอมโพเนนต์ลูกถูกครอบด้วย React.memo และรับฟังก์ชัน callback เป็น prop หากไม่มี useCallback คอมโพเนนต์ลูกจะเรนเดอร์ซ้ำทุกครั้งที่พ่อแม่เรนเดอร์ แม้ว่าข้อมูลของมันจะไม่เปลี่ยนแปลง useCallback ทำให้การอ้างอิงเสถียร และ React.memo สามารถข้ามการเรนเดอร์ซ้ำได้อย่างถูกต้อง
สถานการณ์ที่สองคือ useEffect ที่มี callback ในค่าที่ขึ้นต่อ หากฟังก์ชันถูกส่งไปยังอาร์เรย์ของค่าที่ขึ้นต่อของ useEffect ทุกการอ้างอิงใหม่จะเริ่มต้นเอฟเฟกต์ใหม่ useCallback รับประกันว่าการอ้างอิงเสถียร และเอฟเฟกต์จะทำงานเมื่อข้อมูลจริงเปลี่ยนแปลงเท่านั้น ไม่ใช่ทุกครั้งที่เรนเดอร์ สิ่งนี้สำคัญโดยเฉพาะสำหรับการสมัครรับข้อมูลและคำขอ
// useCallback สำหรับค่าที่ขึ้นต่อของ useEffect ที่เสถียร
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // การอ้างอิงที่เสถียร ไม่ถูกสร้างใหม่เลย
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // เอฟเฟกต์ทำงานเมื่อ props.id เปลี่ยนแปลงเท่านั้น
ความแตกต่างหลักระหว่าง useCallback และ useMemo คือสิ่งที่แต่ละตัวจดจำ useCallback จดจำฟังก์ชัน: useCallback(fn, deps) ส่งคืน fn (เวอร์ชันเดียวกันหรือก่อนหน้า) useMemo จดจำผลลัพธ์ของการเรียกฟังก์ชัน: useMemo(() => computeExpensive(a, b), [a, b]) ส่งคืนค่าที่คำนวณแล้ว ไม่ใช่ฟังก์ชัน
ในทางเทคนิค useCallback คือนํ้าตาลเชิงวากยสัมพันธ์เหนือ useMemo: useCallback(fn, deps) เทียบเท่ากับ useMemo(() => fn, deps) วากยสัมพันธ์นี้มีไว้เพื่อความสามารถในการอ่านเท่านั้น — เพื่อให้นักพัฒนาเห็นชัดเจนว่ากำลังจดจำฟังก์ชัน ไม่ใช่ค่า ไม่มีความแตกต่างด้านประสิทธิภาพระหว่าง useCallback และ useMemo กับฟังก์ชัน — ทั้งคู่สร้างโค้ดที่เหมือนกัน
| ฮุก | จดจำ | วากยสัมพันธ์ | การใช้งาน |
|---|---|---|---|
| useCallback | ฟังก์ชัน (การอ้างอิง) | useCallback(fn, deps) | Callback สำหรับคอมโพเนนต์ลูก |
| useMemo | ผลลัพธ์การคำนวณ | useMemo(() => value, deps) | การคำนวณที่มีต้นทุนสูง การจดจำออบเจกต์ |
// สิ่งเหล่านี้เทียบเท่ากัน:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
ข้อผิดพลาดที่พบบ่อยที่สุดคือ การครอบอย่างไม่มีประโยชน์ ทุกฟังก์ชันใน useCallback โดยไม่มี React.memo บนคอมโพเนนต์ลูก หากคอมโพเนนต์ลูกไม่ได้ถูกครอบด้วย React.memo มันก็ยังคงเรนเดอร์ซ้ำทุกครั้งที่พ่อแม่เรนเดอร์ ไม่ว่าการอ้างอิง callback จะเปลี่ยนหรือไม่ก็ตาม useCallback โดยไม่มี React.memo คือต้นทุนที่ไม่มีประโยชน์
// ❌ ไร้ประโยชน์: ไม่มี React.memo บนลูก
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child ยังคงเรนเดอร์ซ้ำโดยไม่มี React.memo
// ❌ Stale closure: ค่าที่ขึ้นต่อหายไป
const handle = useCallback(() => {
console.log(count); // count เป็น 0 เสมอ — stale closure!
}, []);
// ✅ ถูกต้อง: รวมค่าที่ขึ้นต่อ
const handle = useCallback(() => {
console.log(count);
}, [count]);
ปัญหา stale closure ใน useCallback สามารถแก้ไขได้โดยรวมตัวแปรที่ใช้ทั้งหมดไว้ในอาร์เรย์ของค่าที่ขึ้นต่อ eslint-plugin-react-hooks พร้อม exhaustive-deps จะตรวจสอบโดยอัตโนมัติว่าตัวแปรทั้งหมดจากเนื้อหาของ callback อยู่ในอาร์เรย์ หาก callback ใช้ setState ซึ่งไม่เปลี่ยนแปลงระหว่างการเรนเดอร์ ก็สามารถรวมไว้ใน deps ได้อย่างปลอดภัย — React รับประกันความเสถียรของ setState
คำถามที่พบบ่อย
ไม่ useCallback มีความหมายในสามกรณีเท่านั้น: คอมโพเนนต์ลูกถูกครอบด้วย React.memo ฟังก์ชันถูกใช้ในอาร์เรย์ของค่าที่ขึ้นต่อของ useEffect หรือฟังก์ชันถูกส่งไปยังฮุกแบบกำหนดเองที่ขึ้นอยู่กับความเท่ากันของการอ้างอิง ในกรณีอื่น useCallback เพิ่มค่าใช้จ่ายโดยไม่มีประโยชน์ ทีม React แนะนำให้เขียนโดยไม่มีการปรับแต่งก่อน และเพิ่มตามผลลัพธ์ของการทำโปรไฟล์
สำหรับคอมโพเนนต์ธรรมดา — ฟังก์ชันใหม่ทุกครั้งที่เรนเดอร์เร็วกว่าเล็กน้อย เนื่องจาก useCallback ใช้ทรัพยากรในการเปรียบเทียบค่าที่ขึ้นต่อและจัดสรรอาร์เรย์ สำหรับคอมโพเนนต์ที่มีโครงสร้างต้นไม้ React.memo ลึก useCallback ชนะโดยป้องกันการเรนเดอร์ซ้ำขององค์ประกอบลูกนับพัน วัดและเปรียบเทียบแทนการเดา — ใช้ React DevTools Profiler เพื่อการประเมินตามวัตถุประสงค์
ได้ useCallback ทำงานกับฟังก์ชัน async เช่นเดียวกับฟังก์ชันซิงโครนัส ฮุกจดจำตัวฟังก์ชันเอง และผลลัพธ์ (Promise) จะถูกส่งคืนทุกครั้งที่เรียกใช้ ฟังก์ชัน async ภายใน useCallback เป็นรูปแบบทั่วไปสำหรับ callback โหลดข้อมูลที่เสถียรซึ่งใช้ใน useEffect: const fetchData = useCallback(async (id) => {...}, [])
ใช้ React DevTools Profiler — มันแสดงว่าคอมโพเนนต์ใดเรนเดอร์ซ้ำและเพราะอะไร สำหรับการตรวจสอบเชิงโปรแกรม ให้เพิ่ม console.log หรือใช้ useWhyDidYouUpdate — ไลบรารีที่บันทึกสาเหตุของการเรนเดอร์ซ้ำ สาเหตุหลัก: prop เปลี่ยน (รวมถึงการอ้างอิง callback) สถานะเปลี่ยน หรือคอนเท็กซ์เปลี่ยน หาก useCallback ไม่ช่วย — ตรวจสอบว่าค่าที่ขึ้นต่อทั้งหมดถูกระบุอย่างถูกต้อง
แม้ไม่มี React.memo useCallback ก็มีประโยชน์เมื่อใช้กับ useMemo สำหรับค่าคอนเท็กซ์ หากคุณส่งออบเจกต์ที่มีฟังก์ชันไปยัง Context.Provider ให้ครอบการสร้างออบเจกต์ใน useMemo และแต่ละฟังก์ชันใน useCallback ซึ่งจะป้องกันการเรนเดอร์ซ้ำของผู้ใช้คอนเท็กซ์ทั้งหมดเมื่อฟังก์ชันใดฟังก์ชันหนึ่งเปลี่ยนแปลง แต่สำหรับการส่ง callback โดยตรงใน props โดยไม่มี React.memo จะไม่มีประโยชน์จาก useCallback
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ