- ทำความเข้าใจว่า useState รักษาและอัปเดตสถานะของคอมโพเนนต์ภายในอย่างไร รวมถึงการอัปเดตฟังก์ชันและการจัดการอ็อบเจ็กต์
- ใช้ useEffect สำหรับผลข้างเคียงที่มีตรรกะการตั้งค่า/การล้างข้อมูลที่ชัดเจนและอาร์เรย์การพึ่งพาที่แม่นยำเพื่อหลีกเลี่ยงการรั่วไหลและลูป
- ผสานการใช้งาน useState และ useEffect เข้าด้วยกันสำหรับงานจริง เช่น การดึงข้อมูล การสมัครรับข้อมูล และการอัปเดต DOM ในคอมโพเนนต์แบบฟังก์ชัน
- ปฏิบัติตามกฎของ hooks และจัดการเอฟเฟกต์เป็นกระบวนการ "หลังการเรนเดอร์" เพื่อให้คอมโพเนนต์ของ React สามารถคาดเดาได้และบำรุงรักษาได้ง่าย
React Hooks ได้เปลี่ยนแปลงวิธีการเขียนคอมโพเนนต์ไปอย่างสิ้นเชิงและการเรียนรู้ useState และ useEffect โดยพื้นฐานแล้วมันคือตั๋วเข้าสู่การเขียนโค้ด React สมัยใหม่ หากคุณใช้งานอยู่แล้วแต่ยังติดปัญหาเรื่องลูปไม่สิ้นสุด สถานะที่ล้าสมัย หรืออาร์เรย์การพึ่งพาที่ซับซ้อน คู่มือนี้จะช่วยคุณเชื่อมต่อส่วนที่ขาดหายไปทั้งหมดได้อย่างเป็นรูปธรรม
ในบทความนี้ เราจะเจาะลึกถึงวิธีการใช้งานอย่างถูกต้อง useState และ useEffect ร่วมกันรวมถึงเหตุผลที่นำฮุกมาใช้ตั้งแต่แรก กฎเกณฑ์และข้อควรระวังอย่างเป็นทางการ วิธีการทำงานของความสัมพันธ์ระหว่างส่วนประกอบต่างๆ ในเชิงลึก ข้อผิดพลาดทั่วไปที่ทำให้ส่วนประกอบของคุณเสียหาย และรูปแบบที่ผ่านการทดสอบมาแล้วสำหรับการจัดการผลข้างเคียง การล้างข้อมูล และการจัดการสถานะในโครงการจริง
เหตุใดจึงต้องใช้ hooks และโดยเฉพาะอย่างยิ่ง useState และ useEffect?
Hooks ถูกเพิ่มเข้ามาใน React 16.8 เพื่อให้คอมโพเนนต์แบบฟังก์ชันสามารถใช้คุณสมบัติสถานะและวงจรชีวิตได้โดยไม่ต้องใช้คลาสก่อนหน้านี้ คุณต้องเขียนคอมโพเนนต์แบบคลาสเพื่อเก็บสถานะภายใน รับข้อมูลจากภายนอก หรือตอบสนองต่อเหตุการณ์วงจรชีวิต เช่น การติดตั้งและถอดการติดตั้ง
ปัญหาใหญ่ของการใช้คลาสคือตรรกะที่เกี่ยวข้องมักถูกแบ่งออกไปในเมธอดวงจรชีวิตหลายๆ เมธอด เช่น componentDidMount, componentDidUpdate และ componentWillUnmountคุณจะได้ส่วนประกอบของฟีเจอร์เดียวกันกระจัดกระจายไปตามวิธีการต่างๆ โดยขึ้นอยู่กับ... โดยหมายถึง พวกเขาวิ่งแทนที่จะ อะไร ซึ่งก็เป็นเช่นนั้น ทำให้โค้ดอ่านยาก ทดสอบยาก และนำกลับมาใช้ใหม่ได้ยากขึ้น
ตะขอช่วยพลิกโมเดลนี้กลับด้าน: กับ useState คุณแนบสถานะเข้ากับส่วนประกอบฟังก์ชันโดยตรง และด้วย useEffect คุณเชื่อมโยงผลข้างเคียงเข้ากับตรรกะที่ต้องการโดยตรง ด้วยวิธีนี้ คุณสามารถจัดกลุ่มทุกอย่างที่เกี่ยวข้องกับปัญหาเดียวไว้ในที่เดียว และดึงส่วนเชื่อมต่อที่นำกลับมาใช้ใหม่ได้ง่ายในภายหลัง
ในบรรดาตะขอทั้งหมด useState และ useEffect คือองค์ประกอบพื้นฐานหลักคุณสามารถสร้างฟีเจอร์ทั่วไปส่วนใหญ่ได้ด้วยเพียงแค่สองสิ่งนี้: สถานะ UI เช่น ฟอร์มและปุ่มสลับ, การร้องขอเครือข่าย, การสมัครรับข้อมูล, ตัวจับเวลา, การอัปเดต DOM และอื่นๆ hook อื่นๆ (useRef, useReducer, useContext, useMemo…) นั้นยอดเยี่ยม แต่พวกมันสร้างขึ้นบนพื้นฐานของแนวคิดเดียวกัน
กฎของ React Hooks ที่คุณห้ามฝ่าฝืนเด็ดขาด
React Hooks มาพร้อมกับกฎเกณฑ์ที่เข้มงวดสองสามข้อเพื่อให้ทำงานได้อย่างน่าเชื่อถือในการเรนเดอร์ต่างๆหากคุณละเมิดข้อกำหนดเหล่านี้ คุณอาจพบข้อผิดพลาดขณะรันไทม์ หรือข้อผิดพลาดที่ซับซ้อนและยากต่อการแก้ไข
กฎข้อแรก: เรียกใช้ hook เฉพาะภายในฟังก์ชันคอมโพเนนต์ของ React หรือ hook ที่กำหนดเองเท่านั้นคุณไม่สามารถใช้ได้ useState or useEffect ไม่ว่าจะอยู่ในคอมponent ของคลาส ฟังก์ชันยูทิลิตี้ทั่วไป หรืออยู่นอกคอมponent ใดๆ รูปแบบเช่นนี้ก็ไม่ถูกต้อง:
import React, { Component, useState } from 'react';
class App extends Component {
// ❌ This will throw - hooks don’t work in classes
const = useState(0);
render() {
return <h1>Hello, I am a Class Component!</h1>;
}
}
วิธีที่ถูกต้องคือการเปลี่ยนไปใช้คอมโพเนนต์ฟังก์ชันหากคุณต้องการใช้ฮุก:
import React, { useState } from 'react';
function App() {
const = useState('');
return (
<div>
Your JSX code goes in here...
</div>
);
}
export default App;
กฎข้อที่สอง: เรียกใช้ hook เฉพาะที่ระดับบนสุดของคอมโพเนนต์ของคุณเท่านั้นนั่นหมายความว่าห้ามใช้ hook ภายในลูป เงื่อนไข หรือฟังก์ชันซ้อนกัน React อาศัยการเรียก hook ในลำดับเดียวกันทุกครั้งที่เรนเดอร์เพื่อให้ "ตรงกัน" แต่ละส่วน useState และ useEffect เรียกใช้ด้วยข้อมูลที่จัดเก็บไว้ ดังนั้นจึงไม่ถูกต้อง:
function BadComponent({ enabled }) {
if (enabled) {
// ❌ Wrong: hook inside a conditional
const = useState(0);
}
// ...
}
แทนที่จะทำเช่นนั้น ให้ประกาศ hook โดยไม่มีเงื่อนไขไว้ที่ด้านบนสุด และใช้เงื่อนไขภายใน effect หรือ JSXฟังก์ชัน hook จะต้องถูกเรียกใช้เสมอ แต่ตรรกะที่ทำงานในฟังก์ชันนั้นสามารถเป็นแบบมีเงื่อนไขได้:
function ConditionalEffectComponent() {
const = useState(false);
useEffect(() => {
if (isMounted) {
console.log('Component mounted');
}
}, );
return (
<div>
<button onClick={() => setIsMounted(!isMounted)}>
{isMounted ? 'Unmount' : 'Mount'}
</button>
</div>
);
}
กฎโดยนัยข้อที่สามคือ ต้องนำเข้า hooks จาก React (หรือไลบรารี hook) ไม่ใช่เขียนขึ้นเองแบบเฉพาะกิจนี่เป็นเรื่องที่ชัดเจนอยู่แล้ว แต่ก็ควรกล่าวถึง: เคล็ดลับสำคัญอยู่ที่ตัวจัดการ hook ภายในของ React ที่คอยติดตามการเรียกใช้ hook ในแต่ละรอบการเรนเดอร์
การจัดการสถานะภายในเครื่องอย่างถูกต้องด้วย useState
useState ช่วยให้คุณสามารถแนบสถานะเข้ากับส่วนประกอบฟังก์ชัน และรับทั้งค่าปัจจุบันและฟังก์ชันอัปเดตได้ในเชิงแนวคิดแล้ว มันคือสิ่งที่ทำหน้าที่ตรงข้ามกับ this.state และ this.setState ในส่วนประกอบของคลาส
ตัวอย่างค้านขั้นต่ำที่มี useState หน้าตาแบบนี้:
import React, { useState } from 'react';
function Counter() {
const = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
เมื่อคุณโทร useState(initialValue)React จะจัดเก็บสถานะนั้นและส่งคืนค่าเป็นคู่: ค่าสถานะปัจจุบันและตัวกำหนดค่า ต่างจากตัวแปรโลคอลทั่วไป สถานะจะคงอยู่แม้หลังจากการเรนเดอร์เสร็จสิ้นแล้ว count ค่าจะไม่ถูกรีเซ็ตเป็น 0 ทุกครั้งที่ฟังก์ชันคอมโพเนนต์ทำงาน
คุณสามารถใช้ค่าใดๆ ก็ได้ที่สามารถแปลงเป็นรูปแบบอนุกรมได้สำหรับสถานะ เช่น ตัวเลข สตริง บูลีน อาร์เรย์ อ็อบเจ็กต์ และแม้แต่ฟังก์ชัน. คุณยังสามารถโทร useState มีการใช้หลายครั้งในส่วนประกอบเดียวกัน เพื่อแยกค่าที่เกี่ยวข้องออกจากกัน แทนที่จะรวมทุกอย่างไว้ในวัตถุเดียว
เมื่อค่าสถานะใหม่ขึ้นอยู่กับค่าสถานะก่อนหน้า ให้ใช้แบบฟอร์มการอัปเดตฟังก์ชันเสมอวิธีนี้ช่วยหลีกเลี่ยงข้อผิดพลาดเมื่อมีการอัปเดตสถานะหลายครั้งติดต่อกันอย่างรวดเร็ว:
setCount(prev => prev + 1);
อีกรายละเอียดเล็กน้อยแต่สำคัญคือ การเรียกใช้เมธอด setter จะแทนที่ค่าสถานะทั้งหมด ไม่ใช่การรวมอ็อบเจ็กต์เข้าด้วยกัน this.setState ในชั้นเรียนหากสถานะของคุณเป็นอ็อบเจ็กต์หรืออาร์เรย์ คุณต้องกระจายค่าก่อนหน้าด้วยตนเอง:
const = useState({ name: 'Alex', age: 30 });
// ✅ Correct: copy and update
setUser(prev => ({ ...prev, age: prev.age + 1 }));
สำหรับค่าเริ่มต้นที่มีราคาแพง คุณสามารถกำหนดค่าเริ่มต้นสถานะแบบค่อยเป็นค่อยไปได้โดยการส่งฟังก์ชันเข้าไป useStateReact จะเรียกใช้ฟังก์ชันนี้เฉพาะในการเรนเดอร์ครั้งแรกเท่านั้น:
const = useState(() => calculateInitialValue());
การจัดการผลข้างเคียงด้วย useEffect
useEffect คือ API ของ React สำหรับเรียกใช้งานผลข้างเคียงในคอมโพเนนต์แบบฟังก์ชัน“ผลข้างเคียง” คือสิ่งใดก็ตามที่ส่งผลกระทบต่อโลกภายนอก เช่น การดึงข้อมูล การบันทึกข้อมูล การเปลี่ยนแปลง DOM โดยตรง การสมัครรับข้อมูล ตัวจับเวลา API ของเบราว์เซอร์ เป็นต้น
ตามแนวคิด useEffect แทนที่การรวมกันของ componentDidMount, componentDidUpdate และ componentWillUnmount จากส่วนประกอบของคลาสแทนที่จะแบ่งเอฟเฟกต์หนึ่งเดียวออกเป็นสามเมธอดในวงจรชีวิต คุณประกาศเอฟเฟกต์นั้นเพียงครั้งเดียว แล้วปล่อยให้ React จัดการเองว่าจะเรียกใช้งานเมื่อใดและจะล้างข้อมูลเมื่อใด
ลายเซ็นพื้นฐานคือ useEffect(setup, dependencies?)ส่วน setup ฟังก์ชันนี้คือส่วนเนื้อหาของเอฟเฟกต์ โดยอาจส่งคืนฟังก์ชันสำหรับทำความสะอาดก็ได้ dependencies ตัวแปร array จะบอก React ว่าเมื่อใดที่เอฟเฟกต์จำเป็นต้องทำงานซ้ำอีกครั้ง
useEffect(() => {
// side effect logic here
return () => {
// optional cleanup logic here
};
}, );
โดยค่าเริ่มต้น หากไม่มีอาร์กิวเมนต์ตัวที่สอง เอฟเฟกต์จะทำงานหลังจากทุกครั้งที่มีการเรนเดอร์ (การติดตั้งครั้งแรกและการอัปเดตครั้งต่อๆ ไป) ซึ่งมักจะมากเกินไปสำหรับคำขอเครือข่ายหรือตรรกะที่มีค่าใช้จ่ายสูง
รูปแบบที่พบได้บ่อยมากคือการอัปเดตบางสิ่งภายนอกทุกครั้งที่มีการเปลี่ยนแปลงสถานะตัวอย่างเช่น การอัปเดตชื่อหน้าเว็บตามจำนวนคลิก:
import React, { useState, useEffect } from 'react';
function Counter() {
const = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, ); // effect re-runs only when `count` changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
อาร์เรย์การพึ่งพา (dependency array) มีความสำคัญอย่างยิ่งต่อประสิทธิภาพและความถูกต้องฟังก์ชันนี้ควบคุมว่า React ควรเรียกใช้เอฟเฟกต์อีกครั้งเมื่อใด หากมีการเปลี่ยนแปลงการพึ่งพาใดๆ ตามที่กำหนดไว้ Object.is เมื่อเปรียบเทียบแล้ว เอฟเฟกต์จะถูกล้างและทำงานอีกครั้ง หากไม่มีการเปลี่ยนแปลงใด ๆ ก็จะถูกข้ามไป
เข้าใจโครงสร้าง Dependency Array อย่างมืออาชีพ
อาร์เรย์การพึ่งพาเป็นส่วนที่ละเอียดอ่อนที่สุด useEffect แมลงมาจากReact จะเปรียบเทียบแต่ละองค์ประกอบของอาร์เรย์กับค่าก่อนหน้าโดยใช้ Object.isหากค่าทั้งหมดเท่ากัน ระบบจะข้ามการดำเนินการนั้นไป แต่หากมีค่าอย่างน้อยหนึ่งค่าแตกต่างกัน ระบบจะดำเนินการนั้นซ้ำอีกครั้ง
มีการกำหนดค่าการพึ่งพาหลักๆ สามแบบที่คุณจะใช้ตลอดเวลา:
- ไม่มีข้อโต้แย้งครั้งที่สองเอฟเฟกต์นี้จะทำงานหลังจากเรนเดอร์ทุกครั้ง
- อาร์เรย์ว่าง
[]เอฟเฟกต์นี้จะทำงานเพียงครั้งเดียวเมื่อทำการ mount และจะล้างข้อมูลเมื่อทำการ unmount - อาร์เรย์ที่มีค่า
เอฟเฟกต์นี้จะทำงานหลังจากทำการ mount และทุกครั้งที่มีการเปลี่ยนแปลงการพึ่งพาใดๆ
เมื่อค่าที่พึ่งพาเป็นค่าพื้นฐาน (ตัวเลข สตริง บูลีน) การดำเนินการนี้จะตรงไปตรงมาปัญหาจะเริ่มขึ้นเมื่อคุณใส่วัตถุ อาร์เรย์ หรือฟังก์ชันไว้ภายในตัวแปรที่ขึ้นอยู่กับกันและกัน เพราะการเปรียบเทียบความเท่าเทียมกันนั้นอิงตามการอ้างอิง วัตถุสองชิ้นที่เหมือนกันทุกประการแต่มีการอ้างอิงต่างกันจะถูกพิจารณาว่า "แตกต่างกัน" ซึ่งจะทำให้ต้องรันใหม่ทุกครั้งที่แสดงผล
พิจารณาผลกระทบที่ขึ้นอยู่กับ team วัตถุจากพร็อพ:
function Team({ team }) {
useEffect(() => {
console.log(team.id, team.active);
}, ); // ⚠️ might re-run every render if `team` reference changes
}
แม้ว่าเนื้อหาของทีมจริงจะไม่เปลี่ยนแปลง แต่การอ้างอิงวัตถุใหม่ในแต่ละครั้งที่แสดงผลจะทำให้เอฟเฟกต์ทำงานอีกครั้งเพื่อหลีกเลี่ยงปัญหานี้ คุณควรใช้ฟิลด์พื้นฐานที่คุณใช้งานจริง หรือสร้างวัตถุขึ้นใหม่ภายในเอฟเฟกต์นั้นเอง
เวอร์ชันที่ปลอดภัยกว่าจะติดตามเฉพาะสิ่งที่เอฟเฟกต์ต้องการจริงๆ เท่านั้น:
function Team({ team }) {
const { id, active } = team;
useEffect(() => {
console.log(id, active);
}, );
}
หากคุณต้องการวัตถุทั้งหมดภายในเอฟเฟกต์จริงๆ คุณสามารถสร้างมันขึ้นมาใหม่ที่นั่นแทนที่จะใช้เป็นส่วนประกอบที่ต้องพึ่งพาด้วยวิธีนี้ รายการการพึ่งพาจึงยังคงสามารถอิงตามค่าพื้นฐานได้:
function Team({ team }) {
const { id, active, name } = team;
useEffect(() => {
const localTeam = { id, active, name };
// use `localTeam` here
}, );
}
ในกรณีที่เป็นทางเลือกสุดท้าย คุณสามารถใช้การจดจำผลลัพธ์ (memoization) ร่วมกับ useMemo or useCallback สำหรับสิ่งของหรือฟังก์ชั่นที่มีราคาแพงแต่โปรดจำไว้ว่าการใช้ memoization เองก็มีต้นทุน อย่าใช้มันทุกที่ "เผื่อไว้ก่อน" ให้ใช้เฉพาะเมื่อการพึ่งพาเฉพาะอย่างใดอย่างหนึ่งทำให้เกิดปัญหาด้านประสิทธิภาพจริงๆ เท่านั้น
การลบเอฟเฟ็กต์อย่างถูกต้อง
ผลข้างเคียงบางอย่างทำให้ต้องใช้ทรัพยากรบางส่วน ซึ่งจำเป็นต้องปล่อยคืน: การสมัครรับข้อมูล, ซ็อกเก็ต, ช่วงเวลา, การหมดเวลา, ตัวรับฟังเหตุการณ์ ฯลฯ การลืมล้างข้อมูลเหล่านี้อาจนำไปสู่การรั่วไหลของหน่วยความจำหรือการทำงานซ้ำซ้อนได้ง่าย
In useEffectการล้างข้อมูลจะดำเนินการโดยการส่งคืนฟังก์ชันจากเอฟเฟกต์React จะเรียกฟังก์ชันนี้ก่อนที่จะเรียกใช้เอฟเฟกต์อีกครั้งด้วยส่วนประกอบใหม่ และจะเรียกอีกครั้งเป็นครั้งสุดท้ายเมื่อคอมโพเนนต์ถูกยกเลิกการใช้งาน
import { useEffect } from 'react';
function LogMessage({ message }) {
useEffect(() => {
const log = setInterval(() => {
console.log(message);
}, 1000);
return () => {
clearInterval(log);
};
}, );
return <div>logging to console "{message}"</div>;
}
ในตัวอย่างนี้ ทุกครั้ง message เมื่อมีการเปลี่ยนแปลง React จะล้างช่วงเวลาเก่าก่อน จากนั้นจึงตั้งค่าช่วงเวลาใหม่พร้อมข้อความที่อัปเดตแล้วเมื่อส่วนประกอบหายไปจาก UI การล้างข้อมูลครั้งสุดท้ายจะล้างช่วงเวลาดังกล่าวอย่างถาวร
การจับคู่ “การเตรียมการ + การเก็บกวาด” นี้เป็นหัวใจสำคัญของแบบจำลองทางความคิดของ useEffectลองคิดว่าแต่ละเอฟเฟ็กต์เป็นกระบวนการที่แยกจากกันโดยสิ้นเชิง ซึ่งเริ่มต้นในฟังก์ชัน setup และสิ้นสุดอย่างสมบูรณ์ในฟังก์ชัน cleanup React อาจรันรอบ setup/cleanup หลายรอบในระหว่างการพัฒนา (โดยเฉพาะในโหมด Strict Mode) เพื่อทดสอบว่าฟังก์ชัน cleanup ของคุณได้ยกเลิกทุกอย่างจริง ๆ
ตัวอย่างคลาสสิกคือการสมัครรับข้อมูลจากแหล่งภายนอก เช่น API แชท หรือเหตุการณ์ของเบราว์เซอร์ (ดูเพิ่มเติม) การจัดการ onKeyDown ใน React):
useEffect(() => {
function handleClick(event) {
console.log('Clicked', event.clientX, event.clientY);
}
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, []); // runs once on mount, cleans up on unmount
การใช้ useState และ useEffect ร่วมกันสำหรับการดึงข้อมูล
หนึ่งในชุดค่าผสมที่พบได้บ่อยที่สุดในโลกแห่งความเป็นจริงคือการใช้ useState และ useEffect เพื่อดึงข้อมูลจาก APIคุณเก็บข้อมูล (และอาจรวมถึงสถานะการโหลด/ข้อผิดพลาด) ไว้ในสถานะ (state) และทำการร้องขอในเอฟเฟกต์ที่จะทำงานเมื่อคอมโพเนนต์ถูกติดตั้ง หรือเมื่อพารามิเตอร์บางอย่างเปลี่ยนแปลง
รูปแบบพื้นฐานสำหรับการดึงข้อมูลเมื่อทำการ mount จะมีลักษณะดังนี้:
import { useEffect, useState } from 'react';
function FetchItems() {
const = useState([]);
useEffect(() => {
let ignore = false;
async function fetchItems() {
try {
const response = await fetch('/items');
const fetchedItems = await response.json();
if (!ignore) {
setItems(fetchedItems);
}
} catch (error) {
console.error('Error fetching items:', error);
}
}
fetchItems();
return () => {
// avoid updating state if the component unmounted
ignore = true;
};
}, []);
return (
<div>
{items.map(item => (
<div key={item.id ?? item}>{item.name ?? item}</div>
))}
</div>
);
}
ในที่นี้ อาร์เรย์การพึ่งพาที่ว่างเปล่าทำให้มั่นใจได้ว่าคำขอจะทำงานเพียงครั้งเดียวเท่านั้น. ภายใน ignore การใช้ flag เป็นวิธีง่ายๆ ในการหลีกเลี่ยงการตั้งค่าสถานะให้กับคอมโพเนนต์ที่ยังไม่ได้ติดตั้ง ในกรณีที่การร้องขอได้รับการประมวลผลล่าช้า
นอกจากนี้ ยังเป็นเรื่องปกติมากที่จะเพิ่มสถานะการโหลดและแสดงไอคอนหมุนหรือข้อความตัวอย่างขณะที่ข้อมูลกำลังโหลด:
const Statistics = () => {
const = useState([]);
const = useState(true);
useEffect(() => {
const getStats = async () => {
try {
const statsData = await getData();
setStats(statsData);
} finally {
setLoading(false);
}
};
getStats();
}, []);
if (loading) {
return <div>Loading statistics...</div>;
}
return (
<ul>
{stats.map(stat => (
<li key={stat.id}>{stat.label}: {stat.value}</li>
))}
</ul>
);
};
หากคำสั่งค้นหาของคุณขึ้นอยู่กับพารามิเตอร์ (เช่น หมวดหมู่ ตัวกรอง หรือพารามิเตอร์เส้นทาง) ให้เพิ่มพารามิเตอร์นั้นลงในอาร์เรย์การพึ่งพา ดังนั้นเอฟเฟกต์จึงทำงานซ้ำเมื่อมีการเปลี่ยนแปลง:
useEffect(() => {
async function fetchItems() {
const response = await fetch(`/items?category=${category}`);
const data = await response.json();
setItems(data);
}
fetchItems();
}, );
การคิดในแง่ของ “ผลกระทบในทุกการเรนเดอร์” เทียบกับ “วงจรชีวิตของงาน”
หากคุณคุ้นเคยกับส่วนประกอบของคลาส คุณอาจจะอยากลองนึกภาพแผนผังส่วนประกอบเหล่านั้นในใจดู useEffect วิธีการติดตั้ง/อัปเดต/ถอดการติดตั้งแต่โดยทั่วไปแล้ววิธีนั้นจะยิ่งทำให้เกิดความสับสนมากขึ้น รูปแบบความคิดที่ง่ายกว่าคือ “เอฟเฟกต์จะทำงานหลังจากเรนเดอร์เสร็จ และอาจทำการล้างข้อมูลก่อนการทำงานครั้งต่อไป”
ในชั้นเรียน คุณมักจะต้องทำซ้ำตรรกะระหว่างกัน componentDidMount และ componentDidUpdate เพราะคุณต้องการให้เอฟเฟกต์เดียวกันทำงานทั้งตอน mount และตอนอัปเดต ด้วย hooks ปัญหาการทำงานซ้ำซ้อนนั้นจะหายไป: เอฟเฟกต์เดียวครอบคลุมทั้งสองกรณี และ React จะจัดการการล้างข้อมูลระหว่างการทำงานให้เอง
การออกแบบนี้ยังช่วยขจัดข้อผิดพลาดประเภทหนึ่งที่เกี่ยวกับการจัดการการอัปเดตอย่างไม่ถูกต้องอีกด้วยตัวอย่างเช่น ในคอมโพเนนต์คลาสที่ติดตามสถานะออนไลน์ของเพื่อน การลืมสมัครรับข้อมูลซ้ำเมื่อ... (ข้อความต้นฉบับไม่ชัดเจน) เป็นเรื่องง่าย props.friend การเปลี่ยนแปลงต่างๆ อาจทำให้การสมัครใช้งานล้าสมัยหรือเกิดข้อผิดพลาดขณะยกเลิกการเชื่อมต่อ ด้วย useEffect ที่ระบุรายการ friend.id ในฐานะที่เป็นส่วนประกอบที่จำเป็น React จะทำการล้างข้อมูลสำหรับส่วนประกอบเก่าและตั้งค่าสำหรับส่วนประกอบใหม่โดยอัตโนมัติ
โปรดจำไว้ว่าในโหมด Strict Mode สำหรับการพัฒนา React จะเรียกใช้ขั้นตอนการตั้งค่าและการล้างข้อมูลสองครั้งโดยเจตนาเมื่อมีการเรียกใช้งานแอปพลิเคชันเหตุการณ์นี้ไม่ได้เกิดขึ้นในสภาพแวดล้อมการใช้งานจริง แต่เป็นการทดสอบความเครียดที่มีประโยชน์เพื่อยืนยันว่าการล้างข้อมูลของคุณสามารถลบล้างทุกอย่างได้อย่างแท้จริง และเอฟเฟกต์ของคุณสามารถทำงานซ้ำได้อย่างปลอดภัย
การเพิ่มประสิทธิภาพและการแก้ไขปัญหาพฤติกรรมการใช้งานเอฟเฟกต์
เมื่อเอฟเฟกต์ทำงานบ่อยกว่าที่คุณคาดไว้ สิ่งแรกที่ควรตรวจสอบคืออาร์เรย์การพึ่งพาอาจเป็นเพราะการพึ่งพาของตัวแปรเปลี่ยนไปทุกครั้งที่แสดงผล (ซึ่งมักเกิดขึ้นกับอ็อบเจ็กต์/ฟังก์ชันแบบอินไลน์) หรือคุณอาจลืมระบุอาร์เรย์ไปเลย
การบันทึกค่าการพึ่งพาเป็นวิธีที่รวดเร็วในการแก้ไขปัญหา:
useEffect(() => {
console.log('Effect deps:', dep1, dep2);
}, );
หากคุณเห็นบันทึกข้อมูลที่แตกต่างกันในแต่ละครั้ง ให้ตรวจสอบว่าส่วนประกอบใดมีการเปลี่ยนแปลงจริง ๆบ่อยครั้งที่คุณจะพบว่ามีการสร้างออบเจ็กต์แบบอินไลน์หรือฟังก์ชันลูกศรขึ้นใหม่ทุกครั้งที่เรนเดอร์ การย้ายการสร้างออบเจ็กต์ไปไว้ภายในเอฟเฟกต์ หรือการย้ายฟังก์ชันออกไปอยู่นอกคอมโพเนนต์ หรือการใช้เมโมไรเซชันกับฟังก์ชันเหล่านั้น useCallback สามารถทำให้ความสัมพันธ์ระหว่างสิ่งต่างๆ มีเสถียรภาพเมื่อจำเป็น
ลูปอนันต์เกิดขึ้นเมื่อผลลัพธ์ขึ้นอยู่กับค่าหนึ่งและอัปเดตค่าเดียวกันนั้นโดยไม่มีเงื่อนไข. ตัวอย่างเช่น:
useEffect(() => {
setCount(count + 1); // ⚠️ will cause a loop if `count` is a dependency
}, );
แต่ละครั้ง count การเปลี่ยนแปลง ผลกระทบที่เกิดขึ้น การอัปเดต count จากนั้นก็จะกระตุ้นการเรนเดอร์อีกครั้ง และเป็นเช่นนี้เรื่อยไปเพื่อที่จะ打破รูปแบบนั้น ลองพิจารณาดูว่าการอัปเดตสถานะควรอยู่ในรูปแบบของเอฟเฟกต์หรือไม่ ควรจะถูกกระตุ้นโดยการโต้ตอบของผู้ใช้แทน หรือคุณสามารถพึ่งพาค่าอื่นได้หรือไม่
บางครั้งคุณอาจต้องการอ่านค่าล่าสุดของสถานะหรือพร็อพบางอย่างภายในเอฟเฟกต์โดยไม่ให้ค่านั้นกระตุ้นให้เอฟเฟกต์ทำงานซ้ำในสถานการณ์ขั้นสูงเหล่านั้น API รุ่นใหม่ๆ เช่น “เหตุการณ์เอฟเฟกต์” (ผ่าน useEffectEvent (ในเอกสาร React) หรือการอ้างอิงอาจช่วยได้ แต่ในกรณีส่วนใหญ่ การยึดติดกับ dependency เดิมจะปลอดภัยและง่ายกว่า
การนำทุกอย่างมารวมกัน โดยใช้ useState และ useEffect โดยสรุปแล้ว มันก็คือพฤติกรรมหลักๆ ไม่กี่อย่างนั่นเองรักษาขนาดของสถานะให้เล็กและเน้นเฉพาะจุด เลือกใช้การอัปเดตแบบฟังก์ชันเมื่อสร้างสถานะใหม่จากสถานะเก่า จัดโครงสร้างเอฟเฟกต์โดยใช้คู่การตั้งค่า/การล้างข้อมูล ระบุความสัมพันธ์ของอาร์เรย์อย่างชัดเจน และเคารพกฎของฮุกเสมอ เพื่อให้ React สามารถติดตามได้อย่างน่าเชื่อถือว่าอะไรอยู่ตรงไหน เมื่อคุณปฏิบัติตามหลักการเหล่านี้ คอมโพเนนต์ของคุณจะคาดเดาได้ ผลข้างเคียงจะทำงานได้อย่างถูกต้อง และโค้ดเบส React ของคุณจะพัฒนาได้ง่ายขึ้นมากเมื่อแอปของคุณเติบโตขึ้น
