React Hooks คือฟังก์ชันที่ช่วยให้คุณสามารถใช้ state, effect, context และคุณสมบัติอื่นๆ ของ React ในคอมโพนานท์แบบฟังก์ชัน โดยไม่ต้องเขียนคลาส แนะนำใน React 16.8 ฮูกได้เปลี่ยนแปลงแนวทางการพัฒนาอย่างสิ้นเชิง แทนทีรูปแบบที่ซับซ้อนอย่าง HOC และ render props ตามข้อมูลจาก React, 2024 มี 15 ฮูกในตัวที่ครอบคลุมทุกสถานการณ์การจัดการคอมโพนานท์
หัวข้อสำคัญ
React Hooks เป็นชุดของฟังก์ชันที่สร้างอยู่ใน React ที่ช่วยให้คุณสามารถเชื่อมต่อกับกลไกภายในเฟรมเวิร์กได้โดยไม่ต้องสร้างคลาส ก่อน React 16.8 คอมโพนานท์แบบฟังก์ชันสามารถรับ props และคืน JSX เท่านั้น — ตรรกะที่ซับซ้อนใดๆ ต้องใช้คอมโพนานท์แบบคลาส
ปัญหาของคอมโพนานท์แบบคลาสคือ การแยกตรรกะที่เกี่ยวข้องกัน โค้ดที่เกี่ยวข้องกับหนึ่งหน่วยงาน (เช่น การสมัครสมาชิก WebSocket) ถูกบังคับให้แยกระหว่าง componentDidMount, componentDidUpdate และ componentWillUnmount ฮูกแก้ไขปัญหานี้: ตรรกะทั้งหมดถูกจับกลุ่มอยู่ใน useEffect เดียว
ปัญหาที่สองคือ การนำตรรกะมาใช้ใหม่ เพื่อนำตรรกะ state มาใช้ใหม่ระหว่างคอมโพนานท์แบบคลาส คุณต้องใช้ HOC (คอมโพนานท์ระดับสูง) หรือ render props — ทั้งสองรูปแบบสร้างการซ้อนกันพิเศษในต้นไม้ของคอมโพนานท์ คสต้มฮูกแก้ไขปัญหานี้โดยไม่ต้องซ้อนกัน
useState เป็นฮูกพื้นฐานที่สุด มันรับค่าเริ่มต้นและคืนค่าทูเปิ้ล: ค่าปัจจุบันและฟังก์ชันสำหรับอัพเดตมัน แตกต่างจาก this.setState ในคลาส useState ไม่รวมวัตถุ — state ใหม่แทนที่ state ก่อนหน้าอย่างสมบูรณ์สำหรับการเรียกใช้ useState เฉพาะครั้ง
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
setter ของ useState สามารถรับฟังก์ชันที่รับค่าก่อนหน้าและคืนค่าใหม่ รูปแบบฟังก์ชัน เป็นสิ่งจำเป็นเมื่อ state ใหม่คำนวณจาก state ก่อนหน้าและสามารถเรียก setState แบบเป็นชุดได้ — แต่ละครั้งจะได้รับค่าก่อนหน้าปัจจุบัน
useEffect เป็นฮูกสำหรับการทำผลกระทบข้างค้างในคอมโพนานท์แบบฟังก์ชัน มันรับอาร์กิวเมนสองตัว: ฟังก์ชันผลกระทบและอาร์เรย์การพึ่งพา React จะทำงานผลกระทปหลังแต่ละครั้งที่เรนเดอร์ แต่เมื่อระบุการพึ่งพา — เฉพาะเมื่อตัวใดตัวหนึ่งเปลี่ยนแปลง
อาร์เรย์การพึ่งพาที่ว่างเปล่า [] หมายความว่าผลกระทบจะทำงานหนึ่งครั้งหลังการเรนเดอร์ครั้งแรก (คล้ายกับ componentDidMount) การคืนค่าฟังก์ชันจากผลกระทบคือการทำความสะอาดที่ทำงานเมื่อ unmount (คล้ายกับ componentWillUnmount) และก่อนที่จะทำงานผลกระทบอีกครั้ง
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// ทำความสะอาด - ปิด socket
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
การพึ่งพาของ useEffect เป็น ข้อมุมต่อที่เดียว ระหว่างผลกระทบและโลกภายนอก หากผลกระทบใช้ตัวแปรจาก props หรือ state ตัวแปรนั้นต้องอยู่ในอาร์เรย์การพึ่งพา การละเลยกฎนี้เป็นแหล่งของข้อผิดพลาดกับ stale closure
useContext เป็นฮูกสำหรับการอ่านค่า context มันรับวัตถุ context ที่สร้างโดย React.createContext และคืนค่าปัจจุบัน หาก context เปลี่ยนแปลง คอมโพนานท์จะเรนเดอร์ใหม่โดยอัตโนมัติด้วยค่าใหม่
ก่อน useContext การเข้าถึง context ในคอมโพนานท์แบบฟังก์ชันต้องห่อใน Context.Consumer ซึ่งทำให้เกิดการซ้อนกัน useContext กำจัด Consumer อย่างสมบูรณ์ — เพียงเรียกใช้ฮูกที่ใดก็ได้ในคอมโพนานท์แบบฟังก์ชัน
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('สว่าง');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext ไม่ใช่การแทนที่ Redux หรือ state ทั่วโลก React แนะนำให้ใช้ context สำหรับค่าที่เปลี่ยนแปลงไม่บ่อย — เครื่องหมาย, ภาษา, สถานะการตรวจสอบสิทธิ์ สำหรับข้อมูลที่อัพเดตบ่อยครั้ง แต่ละครั้งที่เรนเดอร์ใหม่จะคำนวณผู้ใช้ context ทั้งหมดใหม่ ซึ่งอาจทำให้ประสิทธิภาพลดลง
useReducer เป็นทางเลือกของ useState สำหรับ state ที่มีตรรกะการอัพเดตที่ซับซ้อน มันรับฟังก์ชัน reducer และ state เริ่มต้น คืนค่า state ปัจจุบันและฟังก์ชัน dispatch reducer รับ state ปัจจุบันและ action แล้วคืนค่า state ใหม่ — เป็นฟังก์ชันบริสุทธิ์ที่ไม่มีผลกระทบข้างค้าง
useReducer มีประโยชน์โดยเฉพาะเมื่อหลาย action (set, reset, increment, addItem) จัดการ state แบบเดียว แทนที่จะใช้ useState หลายอันกับ setter ที่แตกต่างกัน — ใช้ reducer เดียวกับประเภท action ที่ชัดเจน ทำให้ตรรกะสามารถคาดการณ์ได้และทดสอบได้ง่าย
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef สร้างวัตถุที่สามารถเปลี่ยนแปลงได้ซึ่งคงอยู่ระหว่างการเรนเดอร์ การเปลี่ยนแปลง current ไม่ทำให้เกิดการเรนเดอร์ใหม่ การใช้งานหลักคือการเข้าถึงองค์ประกอบ DOM และการเก็บค่าที่สามารถเปลี่ยนแปลงได้ (ตัวจับเวลา, ค่าก่อนหน้า, อินสแตนซ์ไลบรารี)
useMemo จำผลลัพธ์ของฟังก์ชัน มันจะคำนวณค่าใหม่เฉพาะเมื่อการพึ่งพาเปลี่ยนแปลง ใช้สำหรับการคำนวณที่มีราคาแพง — การกรองอาร์เรย์ขนาดใหญ่, การเรียงลำดับ, การจัดรูปแบบข้อมูล useCallback เป็นกรณีพิเศษของ useMemo สำหรับการจำผลลัพธ์ฟังก์ชัน
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// โฟกัสอัตโนมัติเมื่อ mount
useEffect(() => {
inputRef.current?.focus();
}, []);
// จำผลลัพธ์ที่กรองแล้ว
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// จำผลลัพธ์ callback
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
กฎที่สำคัญ: useRef, useMemo และ useCallback เป็น เครื่องมือเพิ่มประสิทธิภาพ ไม่ใช่โครงสร้างที่จำเป็น อย่าห่อทุกอย่างใน useMemo — วัดประสิทธิภาพก่อน การเพิ่มประสิทธิภาพก่อนกำหนดทำให้โค้ดซับซ้อน React เรนเดอร์คอมโพนานท์ได้อย่างรวดเร็ว; useMemo จำเป็นเฉพาะเมื่อการคำนวณมีราคาแพงจริง (O(n²) ขึ้นไป)
React ได้แนะนำกฎที่เข้มงวดสองข้อสำหรับฮูก ข้อแรก: เรียกใช้ฮูกเฉพาะในระดับบนสุด — ไม่ใช้ภายในเงื่อนไข, ลูป หรือฟังก์ชันซ้อนกัน ข้อสอง: เรียกใช้ฮูกเฉพาะจากคอมโพนานท์แบบฟังก์ชันของ React หรือคสต้มฮูก ปลั๊กอิน ESLint eslint-plugin-react-hooks จะตรวจสอบทั้งสองกฎโดยอัตโนมัติ
ข้อผิดพลาดที่พบบ่อยที่สุดคือ การละเมิดลำดับของการเรียกใช้ฮูก React อึงพึ่งลำดับของการเรียกใช้ฮูกระหว่างการเรนเดอร์ หากฮูกอยู่ภายในเงื่อนไขและไม่ทำงานในการเรนเดอร์ครั้งใดเรื่องหนึ่ง ฮูกที่ต่อไปทั้งหมดจะเลื่อนทำให้เกิดข้อผิดพลาด
| ข้อผิดพลาด | สิ่งที่เกิดขึ้น | วิธีแก้ |
|---|---|---|
| ฮูกในเงื่อนไข | ลำดับของฮูกหัก | ย้ายตรรกะเงื่อนไขไปไว้ในฮูก |
| Stale closure | ผลกระทบใช้ค่าเก่า | เพิ่มการพึ่งพาทั้งหมดในอาร์เรย์ |
| ลูปไม่สิ้นสุด | useEffect โดยไม่มีการพึ่งพาเปลี่ยน state | ระบุการพึ่งพาหรือใช้ useReducer |
| การเปลี่ยนแปลง ref ใน render | ผลกระทบข้างค้างในตัวฟังก์ชัน | ย้ายการเปลี่ยนแปลงไปไว้ใน useEffect |
ข้อผิดพลาดที่พบบ่อยอันดับสองคือ stale closure ใน useEffect หากผลกระทบใช้ตัวแปรที่ไม่ได้ระบุในการพึ่งพา ผลกระทบจะเก็บค่าของมันไว้ในขณะที่สร้าง closure วิธีแก้คือรวมตัวแปรที่ใช้ทั้งหมดไว้ในอาร์เรย์การพึ่งพาเสมอ หากมีการพึ่งพามากเกินไป ให้พิจารณาแยกผลกระทปออกเป็นหลายส่วน
คำถามที่พบบ่อย
React อึงพึ่งลำดับของการเรียกใช้ฮูก ระหว่างการเรนเดอร์ หากฮูกถูกเรียกใช้ภายในเงื่อนไขและไม่ทำงานในการเรนเดอร์ครั้งใดเรื่องหนึ่ง ฮูกทั้งหมดที่ต่อไปจะเลื่อนที่ — state ของมันปนกันไปมา กฎ ESLint hooks/exhaustive-deps เตือนเกี่ยวกับเรื่องนี้
useLayoutEffect ทำงานแบบซิงโครนัสและจะทำงานก่อนที่เบราว์เซอร์จะแสดงผลการเปลี่ยนแปลง useEffect ทำงานแบบญาสิงโครนัส — หลังการแสดงผล ใช้ useLayoutEffect เมื่อคุณต้องการวัดหรือเปลี่ยนแปลง DOM ก่อนที่ผู้ใช้จะเห็นการเปลี่ยนแปลง (ตำแหน่ง popover, การคืนสภาพการเลื่อนหน้าจอ)
ใช้ AbortController ภายใน useEffect สร้าง controller แล้วส่ง signal ไปยัง fetch และเรียก controller.abort() ในฟังก์ชันทำความสะอาด สำหรับเบราว์เซอร์รุ่นเก่า ใช้แฟลก cancelled: return () => { cancelled = true; } โดยมีการตรวจสอบก่อน setState
คสต้มฮูก คือฟังก์ชัน JavaScript ที่มีคำนำหน้า use และใช้งานฮูกในตัว สร้างเมื่อตรรกะเดียวกันของฮูกถูกใช้ซ้ำในสองคอมโพนานท์หรือมากกว่านั้น ตัวอย่าง: useDebounce, useLocalStorage, useMediaQuery คสต้มฮูกแทนที่ HOC และ render props
ใน Strict Mode React จะทำการ mount และ unmount คอมโพนานท์สองครั้งโดยเจตนาในโหมดการพัฒนาเพื่อค้นหาข้อผิดพลาดที่เกี่ยวกับผลกระทบที่ไม่ได้ทำความสะอาด หากการทำความสะอาดเขียนถูกต้อง การเรียกสองครั้งจะไม่ทำให้เกิดปัญหา สิ่งนี้ไม่เกิดขึ้นในโหมดการผลิต
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม