เมื่อ useEffect เกิดไม่ work

Teerayut Hiruntaraporn
3 min readAug 16, 2021

--

ใน 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 ขึ้นมาแล้ว ก็ลองกดปุ่มเพื่อแสดงผลมันออกมา ผลที่ได้คือ…

ความน่าสนใจคือ ในช่วงแรก สิ่งที่พิมพ์มาถูกต้อง คือ

  1. Render App ถูกพิมพ์ขึ้นมาเพราะมีการเปิด App Component ขึ้นมา
  2. 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 นั่นเอง

ในส่วนของวิธีการแก้ไขนั้น สามารถทำได้โดย

  1. เอา ไปวางไว้นอก function จะได้ไม่ต้องออกมาเป็น Definition ใหม่ตลอดเวลา
  2. ใช้ useCallback แต่ต้องระลึกไว้ว่า มันอาจจะส่งผลด้าน Memory ได้ เพราะมันเก็บ function ลง memory นั่นเอง
const Demo = useCallback(() => {
return (
<div>
<MyComponent id="1" />
</div>
);
}, []);

เผื่อใครสนใจกรณีนี้ก็สามารถดูได้ที่

https://codesandbox.io/s/nifty-grothendieck-ygd13?file=/src/App.js

--

--

Teerayut Hiruntaraporn
Teerayut Hiruntaraporn

No responses yet