เมื่อ useEffect เกิดไม่ work
ใน React นั้น useEffect ถือเป็นฟังก์ชั่นสามัญ สำหรับจัดการ Site-Effect ของกระบวนการแสดงผลบนหน้าเว็บ แต่จะมีอะไรทำให้มันทำงานผิดเพี้ยนไปจากปกติบ้างหล่ะ
ในเบื้องต้นเรามี Root Component เป็นแบบนี้ครับภายในก็จะมี counter โง่ๆ ตัวนึง แล้วก็ component แบบ Demo อีกตัวหนึ่ง
export default function App() {
const [x, y] = useState(0);
console.log("Render App"); const Demo = () => {
return (
<div>
<MyComponent />
</div>
);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{x}
<Test x={x} setBtn={y} />
<Test2 x={x} /> <Demo></Demo>
</div>
);
}
โดยแต่ละ component ที่อยู่ข้างล่างก็จะมีรายละเอียดดังนี้ครับ
function Test(props) {
useEffect(() => {
console.log("test1");
}, [props.setBtn]);
const set = () => {
props.setBtn(props.x + 1);
}; return (
<div>
<button onClick={set}>test</button>
</div>
);
}function Test2({ x }) {
useEffect(() => {
console.log("test2");
}, []);
return <div> Test: {x}</div>;
}const MyComponent = () => {
useEffect(() => {
console.log("tesT");
}, []);return <div> </div>;
};
โดยที่ทั้ง 3 functional component นั้น จะใส่ useEffect ทุกตัว โดยจะมีเฉพาะตัวแรกที่จะใส่ dependency array ( ถ้าใครสงสัยว่าใส่ไปทำไม ท่าน…เข้าใจถูกต้องแล้ว)
เมื่อเราลองรัน app ขึ้นมาแล้ว ก็ลองกดปุ่มเพื่อแสดงผลมันออกมา ผลที่ได้คือ…
ความน่าสนใจคือ ในช่วงแรก สิ่งที่พิมพ์มาถูกต้อง คือ
- Render App ถูกพิมพ์ขึ้นมาเพราะมีการเปิด App Component ขึ้นมา
- test1, test2, tesT ถูกพิมพ์ขึ้นมา จากผลของ useEffect ใน component ต่างๆ (tesT พิมพ์ 2 ครั้งเพราะ มีอยู่ 2 ที่)
แต่หลังจากนั้นกลับมีเหตุการณ์ประหลาดเกิดขึ้น ตัว Render App ทำงานตามปกติ เพราะมีการเปลี่ยน state ทำให้เกิดการ re-render แต่จะเห็นว่า มี tesT โผล่ขึ้นมาพร้อมกับ Render App ด้วย
ทั้งๆ ที่ใช้ useEffect แบบไม่สนใจ dependency ([])
มันเกิดขึ้นได้อย่างไร
แต่อย่างที่ทราบครับว่า เรามี <MyComponent /> อยู่ 2 ที่เราจึงควรที่จะทราบก่อนว่า แล้วตัวที่พิมพ์เกินมานั้นอยู่ที่ใด เลยไปเพิ่ม code มานิดนึง
โดยใส่ id ให้กับ <MyComponent/> สักหน่อย
const MyComponent = ({id}) => {
useEffect(() => {
console.log(`tesT ${id}`);
}, []);return <div> </div>;
};
จากนั้นก็ Assign ให้ MyComponent ใน Demo เป็น 1 ส่วนอีกตัวเป็น 2
const Demo = () => {
return (
<div>
<MyComponent id="1"/>
</div>
);
};
.
.
.
<Test2 x={x} />
<MyComponent id="2"/>
ผลที่ได้คือ…
เราจะเห็นว่า useEffect ของ MyComponent ที่มีปัญหาจะเป็นตัวที่อยู่ใน <Demo /> แล้วมันเป็นอย่างนั้นได้อย่างไร
กระบวนการทำงานของ functional component ทุกครั้งที่มีการ render มันจะไล่อ่านโค้ดใน function ตลอด
เช่น ในกรณีนี้ ถ้าเกิดการ re-render จาก การเปลี่ยน state มันก็จะทำการเรียก <App /> ใหม่ เพื่อทำการ render
ทีนี้สังเกตว่าตัว <Demo> จะเป็น component ที่อยู่ตัวแปรที่เป็น local ของ <App> อีกที
ดังนั้นทุกครั้งที่เรียก <App> เพื่อ re-render จะมีการสร้าง component <Demo> ใหม่เสมอ
เมื่อเป็น component ใหม่ก็ทำให้ต้องสร้าง environment ใหม่ รวมทั้ง Component ลูกตัวใหม่ด้วย
ทำให้ ตัว <MyComponent> ใน context นั้น เป็นตัวใหม่ตลอดเช่นเดียวกัน
useEffect จึงมองเสมือนกับว่า มันพึ่งเริ่มกระบวนการ render ใหม่ทุกครั้ง
ทำให้แม้ว่าจะใส่ [] ใน dependency array ก็ไม่ช่วยให้หยุดการ เรียก function ใน useEffect นั่นเอง
ในส่วนของวิธีการแก้ไขนั้น สามารถทำได้โดย
- เอา ไปวางไว้นอก function จะได้ไม่ต้องออกมาเป็น Definition ใหม่ตลอดเวลา
- ใช้ useCallback แต่ต้องระลึกไว้ว่า มันอาจจะส่งผลด้าน Memory ได้ เพราะมันเก็บ function ลง memory นั่นเอง
const Demo = useCallback(() => {
return (
<div>
<MyComponent id="1" />
</div>
);
}, []);
เผื่อใครสนใจกรณีนี้ก็สามารถดูได้ที่
https://codesandbox.io/s/nifty-grothendieck-ygd13?file=/src/App.js