Custom Hooks ใน React: แยก Logic ออกจาก UI ให้ Reuse ได้จริง
Custom Hook ใน React ไม่ได้มีหน้าที่แค่ย้ายโค้ดออกจาก component แต่ช่วยแยกพฤติกรรมของระบบออกจาก UI อย่างเป็นระบบ ทำให้โค้ดอ่านง่าย ดูแลง่าย และนำกลับมาใช้ซ้ำได้จริง

Custom Hooks ใน React: แยก Logic ออกจาก UI ให้ Reuse ได้จริง
หลายคนเริ่มใช้ Custom Hooks เพราะต้องการลดความยาวของ component หรือย้ายโค้ดที่ซ้ำกันไปไว้อีกไฟล์หนึ่ง แต่แก่นแท้ของ Custom Hook ใน React ไม่ได้มีไว้แค่นั้น
สิ่งที่สำคัญกว่าคือการแยก logic ที่มีพฤติกรรมของตัวเอง ออกจาก UI ที่มีหน้าที่ render เมื่อแบ่งขอบเขตได้ชัด component จะเหลือหน้าที่หลักเพียงการแสดงผล ทำให้อ่านโค้ดแล้วเข้าใจได้ทันทีว่าเกิดอะไรขึ้นบนหน้าจอ โดยไม่ต้องไล่ตาม useEffect ยาว ๆ จนสับสน
ทำไมการแยก Logic ออกจาก UI จึงสำคัญ
ในแอปจริง เรามักมีหลายหน้าที่ต้องใช้ logic คล้ายกัน เช่น การดึงข้อมูลผู้ใช้ บางหน้าต้องมีสถานะ loading บางหน้าต้องรองรับ error และบางหน้าต้อง refetch เมื่อผู้ใช้กดปุ่ม
ถ้าแต่ละ component เขียนสิ่งเหล่านี้ซ้ำเองทั้งหมด ปัญหาที่ตามมาคือ logic จะกระจายไปทั่วโปรเจกต์ เมื่อมีการเปลี่ยน endpoint หรือปรับพฤติกรรมการโหลดข้อมูล ก็ต้องตามแก้หลายจุด เสี่ยงทั้ง bug และความไม่สอดคล้องกันของระบบ
Custom Hook จึงเข้ามาช่วยรวบรวม logic เหล่านี้ไว้ในที่เดียว แล้วเปิด interface ที่เรียบง่ายให้ component ใช้งาน
คำถามสำคัญก่อนสร้าง Custom Hook
ก่อนจะแยก logic ออกมาเป็น hook ควรถามตัวเองก่อนว่า
logic นี้ไม่ขึ้นกับหน้าตาใช่ไหม?
ถ้าคำตอบคือใช่ ก็มีแนวโน้มสูงว่าควรแยกเป็น Custom Hook ได้ เช่น
- การ fetch data
- การอ่านและเขียน
localStorage - การ debounce keyword สำหรับ search
- การตรวจจับขนาดหน้าจอ
- การจัดการ form state
- การ subscribe event จาก browser
logic เหล่านี้ไม่ควรผูกติดกับ component ใด component หนึ่ง เพราะหลายส่วนของระบบมักต้องการใช้ร่วมกัน
การตั้งชื่อ Hook ที่ดี
การตั้งชื่อเป็นอีกเรื่องที่ส่งผลต่อความเข้าใจของทีมอย่างมาก
ตัวอย่างชื่อที่ดีคือ useUser ไม่ใช่ useUserCard
เหตุผลคือ UserCard เป็นชื่อที่สื่อถึง UI หรือหน้าตาการแสดงผล แต่ useUser สื่อถึงพฤติกรรมของข้อมูลผู้ใช้ เช่น การโหลดข้อมูล การคืนค่า loading และ error หรือการ refetch
ชื่อของ hook ควรบอกว่า ให้ความสามารถอะไร มากกว่าจะบอกว่า ใช้กับ component ไหน เพราะถ้าชื่อผูกกับ UI มากเกินไป โอกาส reuse ก็จะลดลงทันที
ตัวอย่างโครงสร้างของ Hook สำหรับดึงข้อมูลผู้ใช้
ตัวอย่างพื้นฐานของ hook ลักษณะนี้อาจเป็นดังนี้
function useUser(userId) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(`/api/users/${userId}`)
setUser(await res.json())
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}, [userId])
useEffect(() => {
refetch()
}, [refetch])
return { user, loading, error, refetch }
}
จุดเด่นของโครงสร้างแบบนี้คือ component ที่เรียกใช้ไม่จำเป็นต้องรู้ว่า fetch อย่างไร จัดการ error แบบไหน หรือ state ภายในเดินอย่างไร มันแค่รับผลลัพธ์ที่ต้องใช้จริง เช่น
- มีข้อมูล
userหรือไม่ - กำลัง
loadingอยู่หรือไม่ - มี
errorหรือไม่ - ถ้าต้องการโหลดใหม่ควรเรียก
refetchอย่างไร
นี่คือแนวคิดของการออกแบบ API ของ hook ให้ นิ่ง, ชัด, และ เข้าใจง่าย
Hook ที่ดีควรซ่อนความซับซ้อนไว้ข้างใน
เป้าหมายของ Custom Hook ไม่ใช่แค่ทำให้ไฟล์ component สั้นลง แต่คือการซ่อนรายละเอียดที่ไม่จำเป็นต่อ UI
component ไม่ควรรู้ว่ามี endpoint อะไร ใช้ fetch หรือ library ไหน หรือจัดการ lifecycle แบบใด เพราะสิ่งเหล่านี้เป็นรายละเอียดของ logic ไม่ใช่ภาระของการ render
เมื่อ hook ทำหน้าที่นี้ได้ดี component จะอ่านง่ายขึ้นอย่างชัดเจน และทีมจะเข้าใจ intent ของโค้ดได้เร็วขึ้น
สัญญาณของ Custom Hook ที่ออกแบบไม่ดี
Custom Hook ที่ไม่ดีมักเกิดจากการแยกเร็วเกินไป เช่น เห็นโค้ดซ้ำเพียงเล็กน้อยก็รีบสร้าง hook ทันที ผลลัพธ์คือเกิด hook จำนวนมากในโปรเจกต์ แต่แทบไม่มีใครอยากใช้ต่อ เพราะมันเฉพาะทางเกินไป
ลักษณะที่ควรระวัง เช่น
- รับ parameter จำนวนมากเกินจำเป็น
- มีชื่อยาวและผูกกับหลายบริบทในครั้งเดียว
- ซ่อน side effect ที่คาดเดาได้ยาก
- เปลี่ยนภายใน hook ครั้งเดียวแล้วกระทบ component หลายตัว
ตัวอย่างชื่ออย่าง useDashboardUserTableFilterModalState มักสะท้อนว่าการ abstraction นั้นอาจยังไม่ถูกจุด เพราะไม่ได้แยก behavior ออกมาอย่างแท้จริง แต่เพียงย้ายความซับซ้อนไปอยู่อีกไฟล์หนึ่งเท่านั้น
หลักคิดที่ใช้ได้จริง: แยกเมื่อซ้ำจริง และแยกที่ Behavior
กฎง่าย ๆ ที่ใช้ได้ดีคือ
- ซ้ำจริงก่อนค่อยแยก
- ตอนแยก ให้แยก behavior ไม่ใช่แค่ syntax
นั่นหมายความว่าอย่าเพิ่งสร้าง hook เพียงเพราะมีโค้ดหน้าตาคล้ายกันเล็กน้อย แต่ควรรอดูให้ชัดว่ามีพฤติกรรมบางอย่างที่หลาย component ต้องใช้ร่วมกันจริง
การแยกที่ดีต้องทำให้โค้ดระดับ business logic หรือ interaction pattern ถูกห่อหุ้มเป็นหน่วยที่มีความหมาย ไม่ใช่แค่รวมบรรทัดโค้ดที่คล้ายกันไว้ในไฟล์ชื่อ useSomething
อย่าลืมกฎพื้นฐานของ React Hooks
แม้จะเป็น Custom Hook ก็ยังอยู่ภายใต้กฎของ React Hooks เหมือนเดิมทุกประการ
สิ่งที่ต้องระวังคือ
- เรียก hook ได้เฉพาะใน component หรือใน hook อื่น
- ห้ามเรียก hook ภายใน
if - ห้ามเรียก hook ภายใน loop
useEffectdependency ต้องใส่อย่างซื่อสัตย์
การลบ dependency เพียงเพื่อให้ warning หายไป อาจทำให้เกิด bug ที่จับยากกว่าเดิม เพราะพฤติกรรมของ state และ effect จะเริ่มไม่ตรงกับความเป็นจริง
ทำให้ Hook ทดสอบง่ายขึ้นด้วยการแยก Pure Function
Custom Hook ที่ reuse ได้จริง ควรทดสอบได้ง่ายด้วย หาก logic ภายในแน่นเกินไปหรือปะปนหลายความรับผิดชอบ ควรพิจารณาแยกส่วนที่เป็นการคำนวณล้วนออกมาเป็น pure function
ตัวอย่างเช่น
- การ format data
- การ validate form
- การ calculate state
ให้ hook รับหน้าที่เชื่อมกับ React lifecycle เช่น state, effect, callback ส่วนงานคำนวณปล่อยให้ function ปกติรับผิดชอบ วิธีนี้ช่วยให้ทั้งการอ่านโค้ดและการเขียน test ง่ายขึ้นมาก
สรุป
Custom Hook คือสัญญาระหว่าง logic กับ UI
UI ไม่จำเป็นต้องรู้ว่า logic ทำงานภายในอย่างไร ขณะเดียวกัน logic ก็ไม่ควรผูกกับหน้าตาของ component ใด component หนึ่ง หากออกแบบเส้นแบ่งนี้ได้ดี component จะเล็กลง โค้ดซ้ำจะลดลง และการ reuse จะเกิดขึ้นจริง
Custom Hook ที่ดีจึงไม่ใช่แค่ไฟล์ที่ขึ้นต้นด้วย use แต่คือ abstraction ที่ช่วยให้ระบบชัดเจนขึ้น ดูแลง่ายขึ้น และทำให้ทุกคนในทีมเข้าใจโค้ดได้เร็วขึ้น