สำรวจ Concurrent Mode ของ React และกลยุทธ์การจัดการข้อผิดพลาดเพื่อสร้างแอปพลิเคชันที่แข็งแกร่งและเป็นมิตรต่อผู้ใช้ เรียนรู้เทคนิคปฏิบัติเพื่อรับมือกับข้อผิดพลาดอย่างนุ่มนวลและสร้างประสบการณ์ผู้ใช้ที่ราบรื่น
การจัดการข้อผิดพลาดใน React Concurrent Mode: สร้าง UI ที่ทนทานและยืดหยุ่น
Concurrent Mode ของ React เปิดโอกาสใหม่ๆ ในการสร้างส่วนต่อประสานผู้ใช้ (UI) ที่ตอบสนองและโต้ตอบได้ดีขึ้น อย่างไรก็ตาม พลังที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง การทำงานแบบอะซิงโครนัสและการดึงข้อมูล ซึ่งเป็นหัวใจสำคัญของ Concurrent Mode นำมาซึ่งจุดที่อาจเกิดความล้มเหลวและรบกวนประสบการณ์ของผู้ใช้ได้ บทความนี้จะเจาะลึกถึงกลยุทธ์การจัดการข้อผิดพลาดที่แข็งแกร่งภายในสภาพแวดล้อม Concurrent ของ React เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณยังคงทนทานและเป็นมิตรต่อผู้ใช้ แม้ต้องเผชิญกับปัญหาที่ไม่คาดคิด
ทำความเข้าใจ Concurrent Mode และผลกระทบต่อการจัดการข้อผิดพลาด
แอปพลิเคชัน React แบบดั้งเดิมทำงานแบบซิงโครนัส หมายความว่าการอัปเดตแต่ละครั้งจะบล็อกเธรดหลักจนกว่าจะเสร็จสมบูรณ์ ในทางกลับกัน Concurrent Mode ช่วยให้ React สามารถขัดจังหวะ หยุดชั่วคราว หรือยกเลิกการอัปเดตเพื่อจัดลำดับความสำคัญของการโต้ตอบของผู้ใช้และรักษาการตอบสนองที่ดี ซึ่งทำได้โดยใช้เทคนิคต่างๆ เช่น time slicing และ Suspense
อย่างไรก็ตาม ธรรมชาติของการทำงานแบบอะซิงโครนัสนี้ทำให้เกิดสถานการณ์ข้อผิดพลาดใหม่ๆ คอมโพเนนต์อาจพยายาม render ข้อมูลที่ยังดึงมาไม่เสร็จ หรือการทำงานแบบอะซิงโครนัสอาจล้มเหลวโดยไม่คาดคิด หากไม่มีการจัดการข้อผิดพลาดที่เหมาะสม ปัญหาเหล่านี้อาจนำไปสู่ UI ที่เสียหายและประสบการณ์ผู้ใช้ที่น่าหงุดหงิด
ข้อจำกัดของ Try/Catch Blocks แบบดั้งเดิมในคอมโพเนนต์ React
แม้ว่าบล็อก try/catch
จะเป็นพื้นฐานสำคัญสำหรับการจัดการข้อผิดพลาดใน JavaScript แต่ก็มีข้อจำกัดภายในคอมโพเนนต์ของ React โดยเฉพาะอย่างยิ่งในบริบทของการ render บล็อก try/catch
ที่วางไว้โดยตรงภายในเมธอด render()
ของคอมโพเนนต จะ *ไม่* สามารถดักจับข้อผิดพลาดที่เกิดขึ้นระหว่างการ render ได้ เนื่องจากกระบวนการ render ของ React เกิดขึ้นนอกขอบเขตของบริบทการทำงานของบล็อก try/catch
พิจารณาตัวอย่างนี้ (ซึ่งจะ *ไม่* ทำงานตามที่คาดไว้):
function MyComponent() {
try {
// ส่วนนี้จะโยนข้อผิดพลาดหาก `data` เป็น undefined หรือ null
const value = data.property;
return {value};
} catch (error) {
console.error("Error during rendering:", error);
return Error occurred!;
}
}
หาก `data` เป็น undefined เมื่อคอมโพเนนต์นี้ถูก render การเข้าถึง `data.property` จะโยนข้อผิดพลาด อย่างไรก็ตาม บล็อก try/catch
จะ *ไม่* ดักจับข้อผิดพลาดนี้ ข้อผิดพลาดจะถูกส่งต่อไปยังคอมโพเนนต์ระดับบนในโครงสร้างของ React และอาจทำให้ทั้งแอปพลิเคชันล่มได้
ขอแนะนำ Error Boundaries: กลไกจัดการข้อผิดพลาดในตัวของ React
React มีคอมโพเนนต์พิเศษที่เรียกว่า Error Boundary ซึ่งออกแบบมาโดยเฉพาะเพื่อจัดการข้อผิดพลาดระหว่างการ render, lifecycle methods และ constructors ของคอมโพเนนต์ลูก Error Boundaries ทำหน้าที่เป็นตาข่ายความปลอดภัย ป้องกันข้อผิดพลาดไม่ให้แอปพลิเคชันทั้งระบบล่มและให้ UI สำรองที่สวยงาม
Error Boundaries ทำงานอย่างไร
Error Boundaries คือ React class components ที่ implement lifecycle methods อย่างใดอย่างหนึ่ง (หรือทั้งสอง) ต่อไปนี้:
static getDerivedStateFromError(error)
: lifecycle method นี้จะถูกเรียกใช้หลังจากที่คอมโพเนนต์ลูกโยนข้อผิดพลาด มันจะได้รับข้อผิดพลาดเป็นอาร์กิวเมนต์และช่วยให้คุณสามารถอัปเดต state เพื่อบ่งชี้ว่ามีข้อผิดพลาดเกิดขึ้นcomponentDidCatch(error, info)
: lifecycle method นี้จะถูกเรียกใช้หลังจากที่คอมโพเนนต์ลูกโยนข้อผิดพลาด มันจะได้รับข้อผิดพลาดและอ็อบเจกต์ `info` ที่มีข้อมูลเกี่ยวกับ component stack ที่เกิดข้อผิดพลาด เมธอดนี้เหมาะสำหรับการบันทึกข้อผิดพลาดหรือการทำ side effects เช่น การรายงานข้อผิดพลาดไปยังบริการติดตามข้อผิดพลาด (เช่น Sentry, Rollbar หรือ Bugsnag)
การสร้าง Error Boundary แบบง่าย
นี่คือตัวอย่างพื้นฐานของคอมโพเนนต์ Error Boundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// อัปเดต state เพื่อให้การ render ครั้งถัดไปแสดง UI สำรอง
return { hasError: true };
}
componentDidCatch(error, info) {
// ตัวอย่าง "componentStack":
// in ComponentThatThrows (created by App)
// in MyErrorBoundary (created by App)
// in div (created by App)
// in App
console.error("ErrorBoundary caught an error:", error, info.componentStack);
// คุณยังสามารถบันทึกข้อผิดพลาดไปยังบริการรายงานข้อผิดพลาดได้
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// คุณสามารถ render UI สำรองที่กำหนดเองได้
return Something went wrong.
;
}
return this.props.children;
}
}
การใช้ Error Boundary
หากต้องการใช้ Error Boundary เพียงแค่ครอบคอมโพเนนต์ใดๆ ที่อาจโยนข้อผิดพลาด:
function MyComponentThatMightError() {
// คอมโพเนนต์นี้อาจโยนข้อผิดพลาดระหว่างการ render
if (Math.random() < 0.5) {
throw new Error("Component failed!");
}
return Everything is fine!;
}
function App() {
return (
);
}
ถ้า MyComponentThatMightError
โยนข้อผิดพลาด Error Boundary จะดักจับข้อผิดพลาดนั้น อัปเดต state ของมัน และ render UI สำรอง ("Something went wrong.") ส่วนที่เหลือของแอปพลิเคชันจะยังคงทำงานได้ตามปกติ
ข้อควรพิจารณาที่สำคัญสำหรับ Error Boundaries
- ความละเอียด (Granularity): วาง Error Boundaries อย่างมีกลยุทธ์ การครอบทั้งแอปพลิเคชันด้วย Error Boundary เพียงอันเดียวอาจดูน่าสนใจ แต่บ่อยครั้งการใช้ Error Boundaries หลายๆ อันเพื่อแยกข้อผิดพลาดและให้ UI สำรองที่เฉพาะเจาะจงกว่านั้นเป็นวิธีที่ดีกว่า ตัวอย่างเช่น คุณอาจมี Error Boundaries แยกกันสำหรับส่วนต่างๆ ของแอปพลิเคชัน เช่น ส่วนโปรไฟล์ผู้ใช้ หรือคอมโพเนนต์แสดงข้อมูลเป็นภาพ
- การบันทึกข้อผิดพลาด (Error Logging): ใช้
componentDidCatch
เพื่อบันทึกข้อผิดพลาดไปยังบริการระยะไกล ซึ่งจะช่วยให้คุณสามารถติดตามข้อผิดพลาดในเวอร์ชันโปรดักชันและระบุส่วนของแอปพลิเคชันที่ต้องให้ความสนใจ บริการต่างๆ เช่น Sentry, Rollbar และ Bugsnag มีเครื่องมือสำหรับการติดตามและรายงานข้อผิดพลาด - UI สำรอง (Fallback UI): ออกแบบ UI สำรองที่ให้ข้อมูลและเป็นมิตรต่อผู้ใช้ แทนที่จะแสดงข้อความข้อผิดพลาดทั่วไป ควรให้บริบทและคำแนะนำแก่ผู้ใช้ ตัวอย่างเช่น คุณอาจแนะนำให้รีเฟรชหน้า ติดต่อฝ่ายสนับสนุน หรือลองดำเนินการอย่างอื่น
- การกู้คืนจากข้อผิดพลาด (Error Recovery): พิจารณาการใช้กลไกการกู้คืนจากข้อผิดพลาด ตัวอย่างเช่น คุณอาจมีปุ่มที่ให้ผู้ใช้ลองดำเนินการที่ล้มเหลวอีกครั้ง อย่างไรก็ตาม ควรระมัดระวังเพื่อหลีกเลี่ยงการวนซ้ำไม่รู้จบโดยต้องแน่ใจว่าตรรกะการลองใหม่นั้นมีมาตรการป้องกันที่เหมาะสม
- Error Boundaries จะดักจับข้อผิดพลาดในคอมโพเนนต์ที่อยู่ *ข้างใต้* มันในโครงสร้างเท่านั้น Error Boundary ไม่สามารถดักจับข้อผิดพลาดภายในตัวเองได้ หาก Error Boundary ล้มเหลวขณะพยายาม render ข้อความข้อผิดพลาด ข้อผิดพลาดนั้นจะถูกส่งต่อไปยัง Error Boundary ที่ใกล้ที่สุดที่อยู่เหนือขึ้นไป
การจัดการข้อผิดพลาดระหว่างการทำงานแบบอะซิงโครนัสด้วย Suspense และ Error Boundaries
คอมโพเนนต์ Suspense ของ React เป็นวิธีการแบบประกาศ (declarative) เพื่อจัดการกับการทำงานแบบอะซิงโครนัส เช่น การดึงข้อมูล เมื่อคอมโพเนนต์ "suspend" (หยุดการ render ชั่วคราว) เนื่องจากกำลังรอข้อมูล Suspense จะแสดง UI สำรอง เราสามารถใช้ Error Boundaries ร่วมกับ Suspense เพื่อจัดการข้อผิดพลาดที่เกิดขึ้นระหว่างการทำงานแบบอะซิงโครนัสเหล่านี้ได้
การใช้ Suspense สำหรับการดึงข้อมูล
ในการใช้ Suspense คุณต้องมีไลบรารีการดึงข้อมูลที่รองรับมัน ไลบรารีเช่น `react-query`, `swr` และโซลูชันที่กำหนดเองบางตัวที่ครอบ `fetch` ด้วยอินเทอร์เฟซที่เข้ากันได้กับ Suspense สามารถทำสิ่งนี้ได้
นี่คือตัวอย่างง่ายๆ โดยใช้ฟังก์ชัน `fetchData` สมมติที่คืนค่า promise และเข้ากันได้กับ Suspense:
import React, { Suspense } from 'react';
// ฟังก์ชัน fetchData สมมติที่รองรับ Suspense
const fetchData = (url) => {
// ... (โค้ดที่โยน Promise เมื่อข้อมูลยังไม่พร้อมใช้งาน)
};
const Resource = {
data: fetchData('/api/data')
};
function MyComponent() {
const data = Resource.data.read(); // โยน Promise หากข้อมูลยังไม่พร้อม
return {data.value};
}
function App() {
return (
Loading...
ในตัวอย่างนี้:
fetchData
เป็นฟังก์ชันที่ดึงข้อมูลจาก API endpoint ซึ่งถูกออกแบบมาให้โยน Promise เมื่อข้อมูลยังไม่พร้อมใช้งาน นี่คือกุญแจสำคัญที่ทำให้ Suspense ทำงานได้อย่างถูกต้องResource.data.read()
พยายามอ่านข้อมูล หากข้อมูลยังไม่พร้อมใช้งาน (promise ยังไม่ resolve) มันจะโยน promise ทำให้คอมโพเนนต์ suspendSuspense
แสดงfallback
UI (Loading...) ในขณะที่กำลังดึงข้อมูลErrorBoundary
ดักจับข้อผิดพลาดใดๆ ที่เกิดขึ้นระหว่างการ render ของMyComponent
หรือระหว่างกระบวนการดึงข้อมูล หากการเรียก API ล้มเหลว Error Boundary จะดักจับข้อผิดพลาดและแสดง UI สำรองของมัน
การจัดการข้อผิดพลาดภายใน Suspense ด้วย Error Boundaries
กุญแจสำคัญในการจัดการข้อผิดพลาดที่แข็งแกร่งด้วย Suspense คือการครอบคอมโพเนนต์ Suspense
ด้วย ErrorBoundary
ซึ่งจะช่วยให้แน่ใจว่าข้อผิดพลาดใดๆ ที่เกิดขึ้นระหว่างการดึงข้อมูลหรือการ render คอมโพเนนต์ภายในขอบเขตของ Suspense
จะถูกดักจับและจัดการอย่างนุ่มนวล
หากฟังก์ชัน fetchData
ล้มเหลวหรือ MyComponent
โยนข้อผิดพลาด Error Boundary จะดักจับข้อผิดพลาดและแสดง UI สำรองของมัน ซึ่งจะป้องกันไม่ให้แอปพลิเคชันทั้งระบบล่มและมอบประสบการณ์ที่เป็นมิตรต่อผู้ใช้มากขึ้น
กลยุทธ์การจัดการข้อผิดพลาดเฉพาะสำหรับสถานการณ์ต่างๆ ใน Concurrent Mode
นี่คือกลยุทธ์การจัดการข้อผิดพลาดเฉพาะสำหรับสถานการณ์ทั่วไปใน Concurrent Mode:
1. การจัดการข้อผิดพลาดในคอมโพเนนต์ React.lazy
React.lazy
ช่วยให้คุณสามารถ import คอมโพเนนต์แบบไดนามิกได้ ซึ่งช่วยลดขนาด bundle เริ่มต้นของแอปพลิเคชันของคุณ อย่างไรก็ตาม การดำเนินการ import แบบไดนามิกอาจล้มเหลวได้ เช่น หากเครือข่ายไม่พร้อมใช้งานหรือเซิร์ฟเวอร์ล่ม
ในการจัดการข้อผิดพลาดเมื่อใช้ React.lazy
ให้ครอบคอมโพเนนต์ที่โหลดแบบ lazy ด้วยคอมโพเนนต์ Suspense
และ ErrorBoundary
:
import React, { Suspense, lazy } from 'react';
const MyLazyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
Loading component...