Project_ErrorBoundaryDemo.jsx
React.js/lessons/17_error_boundaries/projects/Project_ErrorBoundaryDemo.jsx
/**
* Project: 錯誤邊界 Dashboard — Error Boundaries 練習
*
* 學習重點:
* - Class Component 的 Error Boundary
* - getDerivedStateFromError + componentDidCatch
* - 每個 Widget 獨立包裹錯誤邊界
* - 降級 UI 和重試機制
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { Component, useState } from 'react';
// ============================================================
// Error Boundary(Class Component — 必須用 class 才能做)
// ============================================================
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error(`[ErrorBoundary] ${this.props.name || 'Unknown'}:`, error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '24px', borderRadius: '12px', backgroundColor: '#fef2f2',
border: '1px solid #fecaca', textAlign: 'center',
}}>
<div style={{ fontSize: '2rem', marginBottom: '8px' }}>💥</div>
<h3 style={{ color: '#991b1b', margin: '0 0 4px' }}>
{this.props.name || '元件'}載入失敗
</h3>
<p style={{ color: '#b91c1c', fontSize: '0.85rem', margin: '0 0 12px' }}>
{this.state.error?.message}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
style={{
padding: '8px 20px', borderRadius: '8px', border: 'none',
backgroundColor: '#ef4444', color: 'white', cursor: 'pointer',
fontWeight: '600',
}}
>
重試
</button>
</div>
);
}
return this.props.children;
}
}
// ============================================================
// Dashboard Widgets
// ============================================================
const StatsWidget = () => {
const [crashed, setCrashed] = useState(false);
if (crashed) throw new Error('統計元件發生錯誤!');
return (
<WidgetCard title="📊 統計">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
{[
{ label: '使用者', value: '1,234', color: '#4f46e5' },
{ label: '訂單', value: '567', color: '#22c55e' },
{ label: '收入', value: '$12,345', color: '#f59e0b' },
{ label: '成長率', value: '+15%', color: '#06b6d4' },
].map((stat) => (
<div key={stat.label} style={{ textAlign: 'center', padding: '12px', borderRadius: '8px', backgroundColor: '#f8fafc' }}>
<p style={{ margin: '0 0 4px', fontSize: '1.3rem', fontWeight: 'bold', color: stat.color }}>{stat.value}</p>
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>{stat.label}</p>
</div>
))}
</div>
<button onClick={() => setCrashed(true)} style={{
marginTop: '12px', padding: '6px 12px', borderRadius: '6px',
border: '1px solid #fca5a5', backgroundColor: 'white', color: '#ef4444',
cursor: 'pointer', fontSize: '0.75rem', width: '100%',
}}>
模擬此元件崩潰
</button>
</WidgetCard>
);
};
const ChartWidget = () => {
const [crashed, setCrashed] = useState(false);
if (crashed) throw new Error('圖表資料解析錯誤!');
const data = [40, 65, 50, 80, 55, 70, 90];
const days = ['一', '二', '三', '四', '五', '六', '日'];
const maxVal = Math.max(...data);
return (
<WidgetCard title="📈 週銷售額">
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px', height: '120px', padding: '0 8px' }}>
{data.map((val, i) => (
<div key={i} style={{ flex: 1, textAlign: 'center' }}>
<div style={{
height: `${(val / maxVal) * 100}px`, backgroundColor: '#818cf8',
borderRadius: '4px 4px 0 0', transition: 'height 0.3s',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: '4px',
}}>
<span style={{ color: 'white', fontSize: '0.6rem', fontWeight: 'bold' }}>{val}</span>
</div>
<span style={{ fontSize: '0.7rem', color: '#94a3b8', marginTop: '4px', display: 'block' }}>{days[i]}</span>
</div>
))}
</div>
<button onClick={() => setCrashed(true)} style={{
marginTop: '12px', padding: '6px 12px', borderRadius: '6px',
border: '1px solid #fca5a5', backgroundColor: 'white', color: '#ef4444',
cursor: 'pointer', fontSize: '0.75rem', width: '100%',
}}>
模擬此元件崩潰
</button>
</WidgetCard>
);
};
const RecentOrdersWidget = () => {
const [crashed, setCrashed] = useState(false);
if (crashed) throw new Error('無法載入訂單資料!');
const orders = [
{ id: '#001', customer: 'Alice', amount: '$120', status: '已完成' },
{ id: '#002', customer: 'Bob', amount: '$85', status: '處理中' },
{ id: '#003', customer: 'Carol', amount: '$200', status: '已完成' },
];
return (
<WidgetCard title="🛒 最近訂單">
{orders.map((o) => (
<div key={o.id} style={{
display: 'flex', justifyContent: 'space-between', padding: '8px 0',
borderBottom: '1px solid #f1f5f9', fontSize: '0.85rem',
}}>
<span style={{ color: '#64748b' }}>{o.id}</span>
<span style={{ color: '#0f172a' }}>{o.customer}</span>
<span style={{ fontWeight: '600' }}>{o.amount}</span>
<span style={{
padding: '1px 8px', borderRadius: '4px', fontSize: '0.75rem',
backgroundColor: o.status === '已完成' ? '#f0fdf4' : '#fffbeb',
color: o.status === '已完成' ? '#166534' : '#92400e',
}}>{o.status}</span>
</div>
))}
<button onClick={() => setCrashed(true)} style={{
marginTop: '12px', padding: '6px 12px', borderRadius: '6px',
border: '1px solid #fca5a5', backgroundColor: 'white', color: '#ef4444',
cursor: 'pointer', fontSize: '0.75rem', width: '100%',
}}>
模擬此元件崩潰
</button>
</WidgetCard>
);
};
const NotificationsWidget = () => {
return (
<WidgetCard title="🔔 通知">
{['系統更新完成', '新使用者註冊', '訂單已出貨'].map((n, i) => (
<div key={i} style={{ padding: '8px 0', borderBottom: '1px solid #f1f5f9', fontSize: '0.85rem', color: '#475569' }}>
{n}
</div>
))}
<p style={{ color: '#94a3b8', fontSize: '0.75rem', margin: '8px 0 0' }}>
此元件沒有崩潰按鈕 — 展示不受影響
</p>
</WidgetCard>
);
};
const WidgetCard = ({ title, children }) => (
<div style={{
padding: '20px', borderRadius: '12px', backgroundColor: 'white',
border: '1px solid #f1f5f9', height: '100%', boxSizing: 'border-box',
}}>
<h3 style={{ margin: '0 0 16px', fontSize: '1rem', color: '#0f172a' }}>{title}</h3>
{children}
</div>
);
// ============================================================
// App
// ============================================================
function App() {
return (
<div style={{
maxWidth: '900px', margin: '0 auto', padding: '32px 16px',
fontFamily: "'Segoe UI', 'Noto Sans TC', sans-serif",
}}>
<h1 style={{ color: '#0f172a', marginBottom: '4px' }}>Dashboard</h1>
<p style={{ color: '#94a3b8', marginTop: 0, marginBottom: '8px' }}>
每個 Widget 有獨立的 Error Boundary — 一個壞掉不會影響其他的
</p>
<p style={{ color: '#f59e0b', fontSize: '0.8rem', marginTop: 0, marginBottom: '24px', backgroundColor: '#fffbeb', padding: '8px 12px', borderRadius: '8px' }}>
💡 點擊每個 Widget 下方的「模擬崩潰」按鈕來測試錯誤邊界,然後點「重試」恢復
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<ErrorBoundary name="統計">
<StatsWidget />
</ErrorBoundary>
<ErrorBoundary name="圖表">
<ChartWidget />
</ErrorBoundary>
<ErrorBoundary name="訂單">
<RecentOrdersWidget />
</ErrorBoundary>
<ErrorBoundary name="通知">
<NotificationsWidget />
</ErrorBoundary>
</div>
</div>
);
}
export default App;
Artículos relacionados
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
Leer artículo →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
Leer artículo →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
Leer artículo →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
Leer artículo →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
Leer artículo →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
Leer artículo →