# React 19 useEffectEvent และ Activity: API ใหม่พร้อมคำถามสัมภาษณ์งาน 2026 > เจาะลึก useEffectEvent และ Activity component ใน React 19.2 แก้ปัญหา stale closure, pre-rendering เบื้องหลัง พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์ - Published: 2026-06-03 - Updated: 2026-06-03 - Author: SharpSkill - Tags: react, react-19, useEffectEvent, activity, hooks, interview - Reading time: 8 min --- React 19.2 ได้เปิดตัว API ใหม่สองตัวที่แก้ไขปัญหาที่นักพัฒนาเผชิญมายาวนาน: `useEffectEvent` ขจัดปัญหา stale closure ใน effect และ `` เปิดโอกาสให้ pre-render เบื้องหลังได้พร้อมเก็บรักษา state ทั้งสอง API เปิดตัวใน[เดือนตุลาคม 2025](https://react.dev/blog/2025/10/01/react-19-2) และกำลังเปลี่ยนแปลงวิธีที่แอปพลิเคชัน React ระดับ production จัดการ side effect และการนำทาง > **ต้องใช้ React 19.2 ขึ้นไป** > > useEffectEvent และ Activity ต้องใช้ React 19.2 หรือใหม่กว่า อัปเดตด้วย `npm install react@latest react-dom@latest` ปลั๊กอิน ESLint `eslint-plugin-react-hooks@6+` รองรับ useEffectEvent ใน dependency array โดยตรง ## สิ่งที่ useEffectEvent แก้ไข: ปัญหา Stale Closure นักพัฒนา React ทุกคนเคยพบปัญหา stale closure กล่าวคือ effect จับค่าตัวแปร ณ เวลา render และเมื่อค่านั้นเปลี่ยนแปลง effect ยังคงอ้างอิงค่าเดิมอยู่ วิธีแก้ปัญหาแบบคลาสสิกคือใช้ `useRef` เก็บ mutable reference ซึ่งใช้งานได้แต่ยืดยาวและ linter ตรวจจับไม่ได้ พิจารณาแอปพลิเคชันแชทที่ต้องส่ง analytics เมื่อมีข้อความใหม่เข้ามา โดย log ต้องมีค่า `theme` ล่าสุด แต่การเปลี่ยน `theme` ไม่ควรทำให้ต้อง reconnect ห้องแชท: ```tsx // hooks/useChatRoom.ts // Before useEffectEvent: useRef workaround import { useEffect, useRef } from 'react' export function useChatRoom(roomId: string, theme: string) { const themeRef = useRef(theme) themeRef.current = theme useEffect(() => { const connection = createConnection(roomId) connection.on('message', (msg) => { logAnalytics('new_message', { roomId, theme: themeRef.current }) showNotification(msg) }) connection.connect() return () => connection.disconnect() }, [roomId]) } ``` โค้ดนี้ทำงานได้ถูกต้อง แต่ linter จะแจ้งเตือนว่า `theme` ขาดหายจาก dependency array การปิดคำเตือนนี้อาจซ่อนบั๊กอื่นที่แท้จริง และรูปแบบ `useRef` ทำให้ developer คนใหม่ที่อ่านโค้ดต้องไล่ตามว่าทำไม `theme` ถึงอยู่ใน ref แทนที่จะอยู่ใน dependency array ## useEffectEvent: การแยก Reactive Logic ออกจาก Non-Reactive Logic Hook `useEffectEvent` สร้างฟังก์ชันที่มีเสถียรภาพ (stable function) ซึ่งอ่านค่า props และ state ล่าสุดได้เสมอ โดยไม่ทำให้ effect ต้อง re-synchronize ทำหน้าที่แทนที่รูปแบบ `useRef` ด้วย API แบบ declarative ที่ linter เข้าใจโดยตรง ```tsx // hooks/useChatRoom.ts // After useEffectEvent: clean separation of concerns import { useEffect, useEffectEvent } from 'react' export function useChatRoom(roomId: string, theme: string) { const onMessage = useEffectEvent((msg: string) => { logAnalytics('new_message', { roomId, theme }) showNotification(msg) }) useEffect(() => { const connection = createConnection(roomId) connection.on('message', onMessage) connection.connect() return () => connection.disconnect() }, [roomId]) } ``` Effect จะ re-run เฉพาะเมื่อ `roomId` เปลี่ยนเท่านั้น ส่วน callback `onMessage` จะเห็นค่า `theme` ปัจจุบันเสมอโดยไม่ต้องระบุใน dependency [เอกสาร React อย่างเป็นทางการ](https://react.dev/reference/react/useEffectEvent) ระบุอย่างชัดเจนว่า Effect Event ต้องเรียกจากภายใน effect เท่านั้น ห้ามเรียกใช้ระหว่างการ render หรือส่งต่อไปยัง child component ## กฎและข้อจำกัดของ useEffectEvent กฎสามข้อที่ควบคุมการใช้งาน `useEffectEvent`: 1. เรียกใช้ที่ระดับบนสุดของ component หรือ custom hook เท่านั้น ห้ามอยู่ในลูปหรือเงื่อนไข 2. เรียกฟังก์ชันที่ได้คืนมาจากภายใน `useEffect` หรือ Effect Event อื่นเท่านั้น 3. ห้ามส่งผ่านเป็น prop หรือ return จาก hook เพื่อใช้ภายนอก การฝ่าฝืนกฎข้อ 2 จะทำให้เกิดบั๊กที่ตรวจจับยาก เนื่องจาก function identity เปลี่ยนทุกรอบ render ปลั๊กอิน `eslint-plugin-react-hooks@6+` [บังคับใช้ข้อจำกัดเหล่านี้](https://react.dev/reference/react/useEffectEvent)โดยอัตโนมัติ ```tsx // components/SearchTracker.tsx import { useEffect, useEffectEvent, useState } from 'react' interface SearchTrackerProps { query: string userId: string } export function SearchTracker({ query, userId }: SearchTrackerProps) { const [results, setResults] = useState([]) const onSearchComplete = useEffectEvent((resultCount: number) => { analytics.track('search_complete', { query, userId, resultCount, timestamp: Date.now(), }) }) useEffect(() => { const controller = new AbortController() fetchSearchResults(query, controller.signal).then((data) => { setResults(data) onSearchComplete(data.length) }) return () => controller.abort() }, [query]) return } ``` > **กรณีที่ไม่ควรใช้ useEffectEvent** > > useEffectEvent ไม่ใช่ทางลัดสำหรับปิดคำเตือนจาก dependency linter หากค่าใดควบคุมว่า effect ควร re-run เมื่อใด ค่านั้นต้องอยู่ใน dependency array ให้ใช้ Effect Event เฉพาะกับ logic ที่เป็น side action เช่น logging, notification หรือ analytics ที่อ่านค่า reactive แต่ไม่ต้องการ trigger การ re-run ของ effect ## Activity Component: การ Pre-Render เบื้องหลังพร้อมเก็บรักษา State Component `` (เดิมมีชื่อว่า "Offscreen") ควบคุมว่า children จะแสดงผลหรือซ่อน ต่างจาก conditional rendering ที่ทำลาย state หรือ CSS `display: none` ที่ยังคงให้ effect ทำงานอยู่ `` เก็บรักษา state ในขณะเดียวกันก็ cleanup effect และเลื่อนลำดับความสำคัญของ update ลง ```tsx // components/TabLayout.tsx import { useState } from 'react' import { Activity } from 'react' interface Tab { id: string label: string content: React.ReactNode } export function TabLayout({ tabs }: { tabs: Tab[] }) { const [activeTab, setActiveTab] = useState(tabs[0].id) return (
{tabs.map((tab) => ( {tab.content} ))}
) } ``` ผู้ใช้ที่กำลังกรอกฟอร์มในแท็บ "Profile" สามารถสลับไปแท็บ "Settings" แล้วกลับมาโดยไม่สูญเสียข้อมูลที่กรอก เมื่อแท็บถูกซ่อน effect ของแท็บนั้น (timer, subscription, data fetching) จะถูก cleanup เพื่อคืนทรัพยากร เมื่อแท็บกลับมาแสดงอีกครั้ง effect จะ remount และ state จะถูกกู้คืนทันที ## Activity Mode: พฤติกรรมของ Visible และ Hidden Component `` รับ prop `mode` ที่มีสองค่า: | พฤติกรรม | `mode="visible"` | `mode="hidden"` | |----------|-------------------|------------------| | การ render DOM | ปกติ | `display: none` ผ่าน CSS | | State ของ component | ใช้งานอยู่ | เก็บรักษาในหน่วยความจำ | | Effect (useEffect) | Mount แล้ว | ถูก cleanup | | ลำดับความสำคัญของ update | ปกติ | เลื่อนไปทำเมื่อ idle | | Pre-rendering | ไม่มี | Render ที่ลำดับความสำคัญต่ำ | เมื่อ component เริ่มต้นในสถานะ hidden (render ครั้งแรกด้วย `mode="hidden"`) React จะ pre-render ที่ลำดับความสำคัญต่ำโดยไม่ mount effect ทำให้สามารถนำทางได้ทันที เนื่องจากหน้าเป้าหมายถูก render ไว้เบื้องหลังแล้วเมื่อผู้ใช้คลิก ```tsx // components/PrerenderedRoute.tsx import { Activity, Suspense, use } from 'react' interface PrerenderedRouteProps { isActive: boolean dataPromise: Promise } export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) { return ( }> ) } function DashboardContent({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise) return (
) } ``` `` ที่ซ่อนอยู่จะ pre-render dashboard ที่ลำดับความสำคัญ idle เมื่อใช้ร่วมกับ `Suspense` และ `use` API การ fetch ข้อมูลจะเกิดขึ้นเบื้องหลัง เมื่อ `isActive` เปลี่ยนเป็น `true` เนื้อหาจะปรากฏทันทีโดยไม่แสดง loading spinner ## Activity และ TanStack Query: ข้อควรระวังเรื่อง Cache ข้อผิดพลาดที่พบบ่อยกับ `` เกี่ยวข้องกับ [TanStack Query](https://tanstack.com/query/latest) เนื่องจาก `useQuery` พึ่งพา `useEffect` ภายใน query ภายใน `` ที่ซ่อนอยู่จะไม่ทำงาน เพราะ effect ถูก unmount แล้ว ```tsx // components/UserDashboard.tsx import { useQuery, useQueryClient } from '@tanstack/react-query' import { Activity } from 'react' function UserStats() { const { data } = useQuery({ queryKey: ['user-stats'], queryFn: fetchUserStats, }) return } function DashboardWithPrefetch({ showStats }: { showStats: boolean }) { const queryClient = useQueryClient() queryClient.ensureQueryData({ queryKey: ['user-stats'], queryFn: fetchUserStats, }) return ( ) } ``` วิธีแก้ไขตรงไปตรงมา: ย้ายการ prefetch ข้อมูลไปยัง parent component หรือใช้ `queryClient.ensureQueryData` นอกขอบเขตของ `` ข้อมูลที่ cache ไว้จะพร้อมใช้งานเมื่อ component ที่ซ่อนอยู่กลับมาแสดงผล > **ข้อแลกเปลี่ยนด้านหน่วยความจำ** > > Activity แลกหน่วยความจำเพื่อความเร็ว component tree ที่ซ่อนแต่ละชุดจะยังคงอยู่ในหน่วยความจำพร้อม DOM ทั้งหมด สำหรับแอปพลิเคชันที่มี route ซ่อนจำนวนมาก ควรติดตามการใช้หน่วยความจำอย่างสม่ำเสมอ ## การใช้ useEffectEvent ร่วมกับ Activity ทั้งสอง API เสริมกันในรูปแบบการนำทางจริง สถานการณ์ที่พบบ่อย: dashboard แบบแท็บที่แต่ละแท็บมี WebSocket subscription และ analytics tracking ```tsx // components/LiveDashboard.tsx import { useEffect, useEffectEvent, useState } from 'react' import { Activity } from 'react' function LiveFeed({ channel, userId }: { channel: string; userId: string }) { const [messages, setMessages] = useState([]) const onNewMessage = useEffectEvent((msg: Message) => { analytics.track('live_message', { channel, userId }) setMessages((prev) => [...prev, msg]) }) useEffect(() => { const ws = new WebSocket(`wss://api.example.com/${channel}`) ws.onmessage = (event) => { const msg = JSON.parse(event.data) as Message onNewMessage(msg) } return () => ws.close() }, [channel]) return } export function LiveDashboard({ userId }: { userId: string }) { const [activeChannel, setActiveChannel] = useState('general') const channels = ['general', 'alerts', 'metrics'] return (
{channels.map((ch) => ( ))}
) } ``` เมื่อสลับ channel แท็บ feed ที่ถูกซ่อนจะตัดการเชื่อมต่อ WebSocket (effect cleanup ผ่าน ``) แต่ประวัติข้อความยังคงอยู่ใน state Effect Event `onNewMessage` รับประกันว่า analytics จะอ้างอิง `userId` ล่าสุดเสมอโดยไม่บังคับให้ WebSocket reconnect ## คำถามสัมภาษณ์: useEffectEvent และ Activity คำถามเหล่านี้ทดสอบความเข้าใจเกี่ยวกับ API ใหม่และปฏิสัมพันธ์กับ rendering model ของ React ซึ่งพบได้บ่อยมากขึ้นใน[การสัมภาษณ์งาน React](/technologies/react-next/interview-questions/react-hooks) ที่บริษัทซึ่งใช้ React 19.2 **คำถามที่ 1: useEffectEvent แก้ปัญหาอะไรที่ useCallback ทำไม่ได้?** `useCallback` สร้างฟังก์ชันแบบ memoize แต่ยังคงต้องระบุใน dependency array ของ effect หาก dependency ของ callback เปลี่ยน callback ก็เปลี่ยนตาม ซึ่งทำให้ effect re-trigger ส่วน `useEffectEvent` สร้างฟังก์ชันที่อ่านค่าล่าสุดได้เสมอโดยไม่เป็น dependency ของ effect ดังนั้น effect จะไม่ re-run เพราะ Effect Event การแยกนี้ทำไม่ได้ด้วย `useCallback` เพียงอย่างเดียว **คำถามที่ 2: สามารถส่ง Effect Event เป็น prop ไปยัง child component ได้หรือไม่?** ไม่ได้ Effect Event ออกแบบมาให้เรียกจากภายใน `useEffect` หรือ Effect Event อื่นเท่านั้น identity ของฟังก์ชันเปลี่ยนทุกรอบ render ดังนั้นการส่งเป็น prop จะทำให้เกิด re-render ที่ไม่จำเป็นและทำลายแนวคิดของ API [ปลั๊กอิน ESLint บังคับใช้กฎนี้](https://react.dev/reference/react/useEffectEvent)โดยอัตโนมัติ **คำถามที่ 3: Activity แตกต่างจาก conditional rendering และ CSS display:none อย่างไร?** Conditional rendering (`{show && }`) unmount component ทั้งหมด ทำให้ state สูญหาย CSS `display: none` ซ่อนองค์ประกอบทางสายตาแต่ effect ทั้งหมดยังคงทำงาน สิ้นเปลืองทรัพยากร ส่วน `` เก็บรักษา state ไว้ cleanup effect เลื่อน update ไปทำที่ลำดับความสำคัญ idle และสามารถ pre-render เนื้อหาเบื้องหลังได้ **คำถามที่ 4: เกิดอะไรขึ้นกับ useEffect ภายใน Activity ที่ซ่อนอยู่?** เมื่อ `` เปลี่ยนเป็น `mode="hidden"` React จะเรียก cleanup function ของ `useEffect` ทั้งหมด (ค่า return ของ `useEffect`) จะไม่มี effect ใหม่ mount ในขณะที่ซ่อนอยู่ เมื่อ component กลับมาแสดงผล effect จะ remount พร้อม state ที่เก็บรักษาไว้ ด้วยเหตุนี้[ไลบรารี data fetching](/technologies/react-next/interview-questions/react-query-tanstack) ที่พึ่งพา `useEffect` จึงต้องมีกลยุทธ์ prefetching นอกขอบเขตของ Activity **คำถามที่ 5: จะ pre-render route ด้วย Activity และ Suspense ได้อย่างไร?** ครอบ route ด้วย `` โดยมี `` boundary ภายใน ใช้ `use()` API หรือ data source ที่รองรับ Suspense สำหรับการ fetch ข้อมูล React จะ render hidden tree ที่ลำดับความสำคัญต่ำ โดย resolve Suspense boundary เบื้องหลัง เมื่อผู้ใช้นำทางและ `mode` เปลี่ยนเป็น `"visible"` เนื้อหาที่ render เสร็จแล้วจะปรากฏทันทีโดยไม่มี loading state **คำถามที่ 6: useEffectEvent เป็นตัวแทนของ exhaustive-deps lint rule หรือไม่?** ไม่ใช่ [กฎ exhaustive-deps](https://react.dev/reference/react/useEffect) ยังคงมีความสำคัญสำหรับการตรวจจับ dependency ที่ขาดหายอย่างแท้จริง `useEffectEvent` จัดการเฉพาะกรณีที่เจาะจง: logic ที่อ่านค่า reactive แต่ไม่ควรควบคุมว่า effect จะ re-run เมื่อใด (analytics, notification, logging) การใช้เพื่อปิดคำเตือน dependency ทั้งหมดจะซ่อนบั๊กและขัดกับจุดประสงค์ของ API ## สรุป - `useEffectEvent` แทนที่รูปแบบ `useRef` สำหรับปัญหา stale closure ใน effect พร้อมการรองรับ linter โดยตรงใน `eslint-plugin-react-hooks@6+` - Effect Event อ่านค่า props และ state ล่าสุดเสมอโดยไม่ trigger การ re-synchronize ของ effect เหมาะสำหรับ analytics, logging และ notification callback - `` เก็บรักษา state ของ component ในขณะที่ cleanup effect เป็นจุดกึ่งกลางระหว่าง conditional rendering กับ CSS hiding - Activity ที่ซ่อนอยู่จะ pre-render ที่ลำดับความสำคัญ idle ทำให้การนำทางรวดเร็วเมื่อใช้ร่วมกับ `Suspense` และ `use` API - [TanStack Query](/technologies/react-next/interview-questions/react-query-tanstack) และไลบรารีที่พึ่งพา effect ต้อง prefetch นอกขอบเขตของ Activity เนื่องจาก `useEffect` ไม่ทำงานใน hidden mode - ทั้งสอง API มาพร้อม React 19.2 ควรอัปเดต ESLint และ React พร้อมกันเพื่อรองรับ tooling อย่างสมบูรณ์ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/react-next/react-19-useeffectevent-activity-new-apis-interview-questions