React 19 useEffectEvent และ Activity: API ใหม่พร้อมคำถามสัมภาษณ์งาน 2026
เจาะลึก useEffectEvent และ Activity component ใน React 19.2 แก้ปัญหา stale closure, pre-rendering เบื้องหลัง พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์

React 19.2 ได้เปิดตัว API ใหม่สองตัวที่แก้ไขปัญหาที่นักพัฒนาเผชิญมายาวนาน: useEffectEvent ขจัดปัญหา stale closure ใน effect และ <Activity> เปิดโอกาสให้ pre-render เบื้องหลังได้พร้อมเก็บรักษา state ทั้งสอง API เปิดตัวในเดือนตุลาคม 2025 และกำลังเปลี่ยนแปลงวิธีที่แอปพลิเคชัน React ระดับ production จัดการ side effect และการนำทาง
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 ห้องแชท:
// 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 เข้าใจโดยตรง
// 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 อย่างเป็นทางการ ระบุอย่างชัดเจนว่า Effect Event ต้องเรียกจากภายใน effect เท่านั้น ห้ามเรียกใช้ระหว่างการ render หรือส่งต่อไปยัง child component
กฎและข้อจำกัดของ useEffectEvent
กฎสามข้อที่ควบคุมการใช้งาน useEffectEvent:
- เรียกใช้ที่ระดับบนสุดของ component หรือ custom hook เท่านั้น ห้ามอยู่ในลูปหรือเงื่อนไข
- เรียกฟังก์ชันที่ได้คืนมาจากภายใน
useEffectหรือ Effect Event อื่นเท่านั้น - ห้ามส่งผ่านเป็น prop หรือ return จาก hook เพื่อใช้ภายนอก
การฝ่าฝืนกฎข้อ 2 จะทำให้เกิดบั๊กที่ตรวจจับยาก เนื่องจาก function identity เปลี่ยนทุกรอบ render ปลั๊กอิน eslint-plugin-react-hooks@6+ บังคับใช้ข้อจำกัดเหล่านี้โดยอัตโนมัติ
import { useEffect, useEffectEvent, useState } from 'react'
interface SearchTrackerProps {
query: string
userId: string
}
export function SearchTracker({ query, userId }: SearchTrackerProps) {
const [results, setResults] = useState<string[]>([])
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 <ResultsList results={results} />
}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 <Activity> (เดิมมีชื่อว่า "Offscreen") ควบคุมว่า children จะแสดงผลหรือซ่อน ต่างจาก conditional rendering ที่ทำลาย state หรือ CSS display: none ที่ยังคงให้ effect ทำงานอยู่ <Activity> เก็บรักษา state ในขณะเดียวกันก็ cleanup effect และเลื่อนลำดับความสำคัญของ update ลง
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 (
<div>
<nav className="flex gap-2 border-b border-border">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'border-b-2 border-primary' : ''}
>
{tab.label}
</button>
))}
</nav>
{tabs.map((tab) => (
<Activity key={tab.id} mode={activeTab === tab.id ? 'visible' : 'hidden'}>
{tab.content}
</Activity>
))}
</div>
)
}ผู้ใช้ที่กำลังกรอกฟอร์มในแท็บ "Profile" สามารถสลับไปแท็บ "Settings" แล้วกลับมาโดยไม่สูญเสียข้อมูลที่กรอก เมื่อแท็บถูกซ่อน effect ของแท็บนั้น (timer, subscription, data fetching) จะถูก cleanup เพื่อคืนทรัพยากร เมื่อแท็บกลับมาแสดงอีกครั้ง effect จะ remount และ state จะถูกกู้คืนทันที
พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Activity Mode: พฤติกรรมของ Visible และ Hidden
Component <Activity> รับ 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 ไว้เบื้องหลังแล้วเมื่อผู้ใช้คลิก
import { Activity, Suspense, use } from 'react'
interface PrerenderedRouteProps {
isActive: boolean
dataPromise: Promise<DashboardData>
}
export function PrerenderedRoute({ isActive, dataPromise }: PrerenderedRouteProps) {
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent dataPromise={dataPromise} />
</Suspense>
</Activity>
)
}
function DashboardContent({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
const data = use(dataPromise)
return (
<div className="grid grid-cols-3 gap-4">
<MetricsCard data={data.metrics} />
<ChartPanel data={data.charts} />
<RecentActivity items={data.activity} />
</div>
)
}<Activity> ที่ซ่อนอยู่จะ pre-render dashboard ที่ลำดับความสำคัญ idle เมื่อใช้ร่วมกับ Suspense และ use API การ fetch ข้อมูลจะเกิดขึ้นเบื้องหลัง เมื่อ isActive เปลี่ยนเป็น true เนื้อหาจะปรากฏทันทีโดยไม่แสดง loading spinner
Activity และ TanStack Query: ข้อควรระวังเรื่อง Cache
ข้อผิดพลาดที่พบบ่อยกับ <Activity> เกี่ยวข้องกับ TanStack Query เนื่องจาก useQuery พึ่งพา useEffect ภายใน query ภายใน <Activity> ที่ซ่อนอยู่จะไม่ทำงาน เพราะ effect ถูก unmount แล้ว
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity } from 'react'
function UserStats() {
const { data } = useQuery({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return <StatsDisplay data={data} />
}
function DashboardWithPrefetch({ showStats }: { showStats: boolean }) {
const queryClient = useQueryClient()
queryClient.ensureQueryData({
queryKey: ['user-stats'],
queryFn: fetchUserStats,
})
return (
<Activity mode={showStats ? 'visible' : 'hidden'}>
<UserStats />
</Activity>
)
}วิธีแก้ไขตรงไปตรงมา: ย้ายการ prefetch ข้อมูลไปยัง parent component หรือใช้ queryClient.ensureQueryData นอกขอบเขตของ <Activity> ข้อมูลที่ cache ไว้จะพร้อมใช้งานเมื่อ component ที่ซ่อนอยู่กลับมาแสดงผล
Activity แลกหน่วยความจำเพื่อความเร็ว component tree ที่ซ่อนแต่ละชุดจะยังคงอยู่ในหน่วยความจำพร้อม DOM ทั้งหมด สำหรับแอปพลิเคชันที่มี route ซ่อนจำนวนมาก ควรติดตามการใช้หน่วยความจำอย่างสม่ำเสมอ
การใช้ useEffectEvent ร่วมกับ Activity
ทั้งสอง API เสริมกันในรูปแบบการนำทางจริง สถานการณ์ที่พบบ่อย: dashboard แบบแท็บที่แต่ละแท็บมี WebSocket subscription และ analytics tracking
import { useEffect, useEffectEvent, useState } from 'react'
import { Activity } from 'react'
function LiveFeed({ channel, userId }: { channel: string; userId: string }) {
const [messages, setMessages] = useState<Message[]>([])
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 <MessageList messages={messages} />
}
export function LiveDashboard({ userId }: { userId: string }) {
const [activeChannel, setActiveChannel] = useState('general')
const channels = ['general', 'alerts', 'metrics']
return (
<div>
<nav className="flex gap-2">
{channels.map((ch) => (
<button key={ch} onClick={() => setActiveChannel(ch)}>
{ch}
</button>
))}
</nav>
{channels.map((ch) => (
<Activity key={ch} mode={activeChannel === ch ? 'visible' : 'hidden'}>
<LiveFeed channel={ch} userId={userId} />
</Activity>
))}
</div>
)
}เมื่อสลับ channel แท็บ feed ที่ถูกซ่อนจะตัดการเชื่อมต่อ WebSocket (effect cleanup ผ่าน <Activity>) แต่ประวัติข้อความยังคงอยู่ใน state Effect Event onNewMessage รับประกันว่า analytics จะอ้างอิง userId ล่าสุดเสมอโดยไม่บังคับให้ WebSocket reconnect
คำถามสัมภาษณ์: useEffectEvent และ Activity
คำถามเหล่านี้ทดสอบความเข้าใจเกี่ยวกับ API ใหม่และปฏิสัมพันธ์กับ rendering model ของ React ซึ่งพบได้บ่อยมากขึ้นในการสัมภาษณ์งาน React ที่บริษัทซึ่งใช้ 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 บังคับใช้กฎนี้โดยอัตโนมัติ
คำถามที่ 3: Activity แตกต่างจาก conditional rendering และ CSS display:none อย่างไร?
Conditional rendering ({show && <Component />}) unmount component ทั้งหมด ทำให้ state สูญหาย CSS display: none ซ่อนองค์ประกอบทางสายตาแต่ effect ทั้งหมดยังคงทำงาน สิ้นเปลืองทรัพยากร ส่วน <Activity mode="hidden"> เก็บรักษา state ไว้ cleanup effect เลื่อน update ไปทำที่ลำดับความสำคัญ idle และสามารถ pre-render เนื้อหาเบื้องหลังได้
คำถามที่ 4: เกิดอะไรขึ้นกับ useEffect ภายใน Activity ที่ซ่อนอยู่?
เมื่อ <Activity> เปลี่ยนเป็น mode="hidden" React จะเรียก cleanup function ของ useEffect ทั้งหมด (ค่า return ของ useEffect) จะไม่มี effect ใหม่ mount ในขณะที่ซ่อนอยู่ เมื่อ component กลับมาแสดงผล effect จะ remount พร้อม state ที่เก็บรักษาไว้ ด้วยเหตุนี้ไลบรารี data fetching ที่พึ่งพา useEffect จึงต้องมีกลยุทธ์ prefetching นอกขอบเขตของ Activity
คำถามที่ 5: จะ pre-render route ด้วย Activity และ Suspense ได้อย่างไร?
ครอบ route ด้วย <Activity mode="hidden"> โดยมี <Suspense> 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 ยังคงมีความสำคัญสำหรับการตรวจจับ 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
<Activity>เก็บรักษา state ของ component ในขณะที่ cleanup effect เป็นจุดกึ่งกลางระหว่าง conditional rendering กับ CSS hiding- Activity ที่ซ่อนอยู่จะ pre-render ที่ลำดับความสำคัญ idle ทำให้การนำทางรวดเร็วเมื่อใช้ร่วมกับ
SuspenseและuseAPI - TanStack Query และไลบรารีที่พึ่งพา effect ต้อง prefetch นอกขอบเขตของ Activity เนื่องจาก
useEffectไม่ทำงานใน hidden mode - ทั้งสอง API มาพร้อม React 19.2 ควรอัปเดต ESLint และ React พร้อมกันเพื่อรองรับ tooling อย่างสมบูรณ์
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Cache Components ใน Next.js 16 ปี 2026: คู่มือครบถ้วน use cache, PPR และคำถามสัมภาษณ์งาน
คู่มือเชิงลึกเกี่ยวกับ Cache Components ใน Next.js 16: directive use cache ทั้งสามระดับ, Partial Pre-Rendering, cacheLife profiles, cacheTag, ความปลอดภัยของ cache และคำถามสัมภาษณ์งานสำหรับ developer ในปี 2026

React Compiler ในปี 2026: Automatic Memoization และคำถามสัมภาษณ์งาน
เรียนรู้ React Compiler ที่ทำ memoization อัตโนมัติ พร้อมคำถามสัมภาษณ์งานยอดนิยมสำหรับนักพัฒนา React ในปี 2026

React Server Components ในโปรดักชัน: รูปแบบและกับดัก
React Server Components ในโปรดักชัน: รูปแบบที่ผ่านการพิสูจน์ แอนตี้แพทเทิร์นที่พบบ่อย และกลยุทธ์การดีบักสำหรับแอป Next.js 15 ที่มั่นคง