Xử lý ranh giới lỗi một cách chuyên nghiệp
Tại sao cần có ranh giới lỗi?
Trong phát triển frontend, lỗi ở một thành phần có thể khiến toàn bộ ứng dụng sập. Hãy tưởng tượng: người dùng đang điền một biểu mẫu phức tạp, đột nhiên ứng dụng trắng màn hình chỉ vì lỗi nhỏ ở thành phần con - trải nghiệm người dùng lúc này thật tệ!
Ranh giới lỗi (Error Boundary) ra đời để giải quyết vấn đề này. Nó giúp bắt lỗi JavaScript trong cây thành phần con, ghi lại thông tin lỗi và hiển thị giao diện thay thế thay vì để ứng dụng sập hoàn toàn.
Triển khai ranh giới lỗi trong React
Cách cài đặt cơ bản
Chú ý quan trọng: Tính đến React 18, ranh giới lỗi chỉ có thể triển khai qua class component do giới hạn của framework. Tuy nhiên có thể kết hợp với Hooks để sử dụng!
import React from 'react'; // Ranh giới lỗi phải dùng class component
class ErrorBoundary extends React.Component {
state = {
coLoi: false,
thongTinLoi: null,
chiTietLoi: null
};
// Phương thức tĩnh để cập nhật trạng thái khi có lỗi
static getDerivedStateFromError(loi) {
return {
coLoi: true,
thongTinLoi: loi
};
}
// Bắt lỗi và ghi log
componentDidCatch(loi, chiTiet) {
console.error('Ranh giới lỗi bắt được lỗi:', loi, chiTiet);
this.setState({ chiTietLoi: chiTiet });
// Có thể gửi log lỗi lên server
}
// Hàm thiết lập lại trạng thái
thietLapLai = () => {
this.setState({
coLoi: false,
thongTinLoi: null,
chiTietLoi: null
});
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
if (this.state.coLoi) {
// Hỗ trợ UI lỗi tùy chỉnh
return this.props.fallbackUI || (
<div className="gioi-han-loi">
<h2>Xin lỗi, đã có lỗi xảy ra 😢</h2>
<div className="chi-tiet-loi">
<p>{this.state.thongTinLoi?.toString()}</p>
<details>
<summary>Chi tiết lỗi</summary>
{this.state.chiTietLoi?.componentStack}
</details>
</div>
<button onClick={this.thietLapLai} className="nut-thu-lai">
Thử lại
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Kết hợp Hooks với ranh giới lỗi
Dù ranh giới lỗi cần class component, vẫn có thể sử dụng dễ dàng trong các component Hooks:
import React, { useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
// Component có thể gặp lỗi
const ComponentCoLoi = () => {
const [soDem, setSoDem] = useState(0);
if (soDem > 3) {
// Lỗi giả lập
throw new Error('Vượt quá giới hạn đếm!');
}
return (
<div>
<h3>Số hiện tại: {soDem}</h3>
<button onClick={() => setSoDem(soDem + 1)}>Tăng số</button>
</div>
);
};
// Sử dụng ranh giới lỗi
const App = () => {
return (
<div className="ung-dung">
<h1>Ví dụ ranh giới lỗi React</h1>
<ErrorBoundary
// UI lỗi tùy chỉnh
fallbackUI={
<div className="loi-tu-dinh-dang">
<h3>😱 Thành phần có lỗi!</h3>
<p>Thành phần đếm bị lỗi, vui lòng thử lại</p>
</div>
}
>
<ComponentCoLoi />
</ErrorBoundary>
</div>
);
};
export default App;
Tối ưu nâng cao
1. Báo cáo lỗi lên server
Kết hợp cú pháp JS hiện đại và Fetch API:
componentDidCatch(loi, chiTiet) {
// Gửi lỗi lên server
fetch('/api/bao-cao-loi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
loi: loi.toString(),
stack: chiTiet.componentStack,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
})
}).catch(baoCaoLoi => {
console.error('Gửi lỗi thất bại:', baoCaoLoi);
});
}
2. Hỗ trợ hàm thiết lập lại
Trong phiên bản cơ bản đã có chức năng thiết lập lại, cách sử dụng:
<ErrorBoundary
onReset={() => {
// Thiết lập lại trạng thái hoặc dọn dẹp
console.log('Đã thiết lập lại ranh giới lỗi');
}}
>
<ComponentCoLoi />
</ErrorBoundary>
️ Lưu ý quan trọng
Ranh giới lỗi React
- Chỉ bắt lỗi trong cây thành phần con, không bắt lỗi chính nó
- Không bắt lỗi trong mã bất đồng bộ (setTimeout, fetch callback...)
- Không bắt lỗi trong handler sự kiện (onClick...)
- Chỉ hỗ trợ từ React 16 trở lên
- Phải dùng class component (Giới hạn framework)
Xử lý lỗi trong Vue 3
- Hàm
onErrorCapturedchỉ bắt lỗi thành phần con - Bắt lỗi biên dịch và render template
- Mặc định lỗi sẽ lan truyền, trả về
falseđể ngăn chặn - Lỗi bất đồng bộ cần dùng
try/catchhoặc xử lý toàn cục - Vue 3 Composition API cung cấp cách xử lý linh hoạt hơn
Triển khai xử lý lỗi trong Vue 3 (Syntax sugar)
Vue 3 cung cấp Composition API hiện đại, kết hợp <script setup> để xử lý lỗi:
<template>
<div>
<slot v-if="!coLoi"></slot>
<div v-else class="gioi-han-loi">
<h2>Xin lỗi, đã có lỗi xảy ra 😢</h2>
<div class="chi-tiet-loi" v-if="thongTinLoi">
<p>{{ thongTinLoi.message }}</p>
<details>
<summary>Chi tiết lỗi</summary>
{{ thongTinLoi.stack }}
</details>
</div>
<button @click="thietLapLai" class="nut-thu-lai">
Thử lại
</button>
</div>
</div>
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue';
// Định nghĩa props
const props = defineProps({
// UI lỗi tùy chỉnh
fallbackUI: {
type: Object,
default: null
}
});
// Định nghĩa sự kiện
const emit = defineEmits(['reset']);
// Trạng thái lỗi
const coLoi = ref(false);
const thongTinLoi = ref(null);
const chiTietLoi = ref(null);
// Hàm bắt lỗi thành phần con
onErrorCaptured((loi, thietBi, thongTin) => {
console.error('Vue 3 bắt lỗi:', loi, thietBi, thongTin);
coLoi.value = true;
thongTinLoi.value = loi;
chiTietLoi.value = thongTin;
// Gửi log lỗi
baoCaoLoi(loi, thongTin);
// Trả về false để ngăn lỗi lan truyền
return false;
});
// Hàm báo cáo lỗi
const baoCaoLoi = async (loi, thongTin) => {
try {
await fetch('/api/bao-cao-loi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
loi: loi.toString(),
stack: loi.stack,
thongTin,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
})
});
} catch (baoCaoLoi) {
console.error('Gửi lỗi thất bại:', baoCaoLoi);
}
};
// Hàm thiết lập lại
const thietLapLai = () => {
coLoi.value = false;
thongTinLoi.value = null;
chiTietLoi.value = null;
emit('reset');
};
</script>
Ví dụ sử dụng
Ví dụ xử lý lỗi Vue 3
<!-- Sử dụng UI lỗi tùy chỉnh -->
import { h } from 'vue';
import ErrorBoundary from './ErrorBoundary.vue';
import ComponentCoLoi from './ComponentCoLoi.vue';
import ComponentKhacCoLoi from './ComponentKhacCoLoi.vue';
// UI lỗi tùy chỉnh
const uiLoiTuDinhDang = h('div', { class: 'loi-tu-dinh-dang' }, [
h('h3', '📊 Biểu đồ tải lỗi'),
h('p', 'Vui lòng kiểm tra kết nối mạng'),
h('button', { onClick: () => console.log('Nhấn nút thử lại') }, 'Tải lại')
]);
// Hàm xử lý thiết lập lại
const xuLyThietLapLai = () => {
console.log('Đã thiết lập lại, ứng dụng hoạt động bình thường');
};