0
0
Lập trình
TT

Ranh Giới Lỗi Trong React: Xây Dựng Ứng Dụng Bền Vững

Đăng vào 7 tháng trước

• 11 phút đọc

Giới Thiệu

Khi xây dựng ứng dụng React, chúng ta thường tập trung vào những tình huống thuận lợi - đảm bảo mọi thứ hoạt động như mong đợi khi người dùng tương tác với ứng dụng. Nhưng điều gì sẽ xảy ra khi có sự cố xảy ra? Một lỗi không được xử lý có thể làm sập toàn bộ ứng dụng React của bạn, để lại người dùng với một màn hình trắng. Đó là lúc Ranh Giới Lỗi (Error Boundaries) trở thành cứu cánh.

Ranh Giới Lỗi Là Gì?

Ranh Giới Lỗi là các thành phần React có khả năng bắt các lỗi JavaScript ở bất kỳ đâu trong cây thành phần con của chúng, ghi lại những lỗi đó và hiển thị một giao diện người dùng thay thế thay vì cây thành phần đã bị sập. Hãy tưởng tượng chúng như một khối try...catch cho các thành phần React.

Ranh Giới Lỗi có thể bắt lỗi trong:

  • Giai đoạn Render
  • Trong các phương thức vòng đời
  • Trong các hàm khởi tạo của toàn bộ cây phía dưới

Những Lỗi Mà Ranh Giới Lỗi KHÔNG Bắt

Điều quan trọng là hiểu rõ những giới hạn:

  • Các trình xử lý sự kiện (sử dụng try...catch thông thường cho những điều này)
  • Mã bất đồng bộ (ví dụ: setTimeout hoặc requestAnimationFrame)
  • Các lỗi xảy ra trong quá trình render phía máy chủ
  • Các lỗi được ném trong chính ranh giới lỗi

Xây Dựng Một Ranh Giới Lỗi Cơ Bản

Hãy bắt đầu với một triển khai đơn giản bằng TypeScript:

typescript Copy
import React, { Component, type ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    // Cập nhật trạng thái để lần render tiếp theo sẽ hiển thị giao diện thay thế
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Ghi lại lỗi vào dịch vụ báo cáo lỗi của bạn
    console.error('Lỗi bị bắt bởi Ranh Giới Lỗi:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div style={{
          padding: '20px',
          border: '1px solid #ff6b6b',
          borderRadius: '8px',
          backgroundColor: '#ffe0e0',
          color: '#d63031',
          textAlign: 'center',
          margin: '20px'
        }}>
          <h2>Ôi! Có gì đó không đúng</h2>
          <p>Chúng tôi xin lỗi vì sự bất tiện. Vui lòng thử làm mới trang.</p>
          <button
            onClick={() => window.location.reload()}
            style={{
              padding: '8px 16px',
              backgroundColor: '#d63031',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            Làm Mới Trang
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Ranh Giới Lỗi Nâng Cao Với Chức Năng Đặt Lại

Đối với các ứng dụng sản xuất, bạn sẽ muốn xử lý lỗi một cách tinh vi hơn. Đây là một phiên bản nâng cao:

typescript Copy
import { Component } from "react";

type FallbackRenderArgs = {
  error: Error;
  reset: () => void;
}

type Props = {
  children: React.ReactNode;
  fallback?: React.ReactNode;
  fallbackRender?: (args: FallbackRenderArgs) => React.ReactNode;
  resetKeys?: unknown[];
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
  onReset?: () => void;
}

type State = {
  hasError: boolean;
  error?: Error | null;
}

export default class ErrorBoundary extends Component<Props, State> {
  state: State = {hasError: false, error: null};
  private prevResetKeys: unknown[] = [];

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  componentDidUpdate(prevProps: Props) {
    if (this.props.resetKeys && this.haveResetKeysChanged(prevProps.resetKeys || [], this.props.resetKeys || [])) {
      this.reset();
    }
  }

  private haveResetKeysChanged(prevKeys: unknown[], nextKeys: unknown[]) {
    return prevKeys.length !== nextKeys.length ||
           prevKeys.some((key, index) => key !== nextKeys[index]);
  }

  private reset = () => {
    this.setState({ hasError: false, error: null });
    this.props.onReset?.();
  }

  render() {
    if (this.state.hasError) {
      const {fallback, fallbackRender} = this.props;

      if (fallbackRender) {
        return fallbackRender({ error: this.state.error!, reset: this.reset });
      }

      if (fallback) {
        return fallback;
      }

      return (
        <div style={{
          padding: '20px',
          backgroundColor: '#fff3cd',
          border: '1px solid #ffeaa7',
          borderRadius: '8px',
          color: '#856404',
        }}>
          <h3>Có gì đó không đúng.</h3>
          <p>{this.state.error?.message}</p>
          <button onClick={this.reset}>Thử lại</button>
        </div>
      )
    }

    return this.props.children;
  }
}

Tạo Các Loại Lỗi Tùy Chỉnh

Để xử lý lỗi tốt hơn, hãy tạo các lớp lỗi tùy chỉnh với thông tin bổ sung:

typescript Copy
export class MyAppError extends Error {
  details?: {
    code?: string;
    section?: string;
    payload?: unknown;
  };

  constructor(message: string, details?: MyAppError['details']) {
    super(message);
    this.name = 'MyAppError';
    this.details = details;
  }
}

Sử Dụng Ranh Giới Lỗi Trong Thực Tiễn

Dưới đây là cách triển khai ranh giới lỗi trong ứng dụng của bạn:

typescript Copy
function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* Sử dụng cơ bản với giao diện thay thế mặc định */}
      <ErrorBoundary>
        <SomeComponent />
      </ErrorBoundary>

      {/* Với giao diện thay thế tùy chỉnh */}
      <ErrorBoundary
        fallback={
          <div>
            <h3>Giao Diện Lỗi Tùy Chỉnh</h3>
            <p>Có gì đó không đúng trong phần này!</p>
          </div>
        }
      >
        <AnotherComponent />
      </ErrorBoundary>

      {/* Với chức năng đặt lại */}
      <ErrorBoundary
        resetKeys={[count]}
        onError={(error) => console.log('Đã ghi lại vào dịch vụ giám sát:', error)}
        onReset={() => console.log('Đã đặt lại ranh giới')}
        fallbackRender={({ error, reset }) => (
          <div>
            <h3>Lỗi: {error.name}</h3>
            <p>{error.message}</p>
            <button onClick={reset}>Thử Lại</button>
          </div>
        )}
      >
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}

Các Thực Hành Tốt Nhất

1. Đặt Vị Trí Chiến Lược

Đặt ranh giới lỗi tại các điểm chiến lược trong cây thành phần:

  • Xung quanh các thành phần định tuyến
  • Xung quanh các phần UI chính
  • Xung quanh các thành phần bên thứ ba

2. Xử Lý Lỗi Tinh Vi

Không bao bọc toàn bộ ứng dụng của bạn trong một ranh giới lỗi duy nhất. Sử dụng nhiều ranh giới cho các phần khác nhau:

typescript Copy
function App() {
  return (
    <div>
      <Header /> {/* Không được bao bọc - lỗi ở header không nên làm sập điều hướng */}

      <ErrorBoundary>
        <Navigation />
      </ErrorBoundary>

      <main>
        <ErrorBoundary>
          <MainContent />
        </ErrorBoundary>
      </main>

      <ErrorBoundary>
        <Sidebar />
      </ErrorBoundary>
    </div>
  );
}

3. Cung Cấp Thông Điệp Lỗi Có Ý Nghĩa

Cung cấp thông điệp lỗi hữu ích và các tùy chọn phục hồi:

typescript Copy
<ErrorBoundary
  fallbackRender={({ error, reset }) => (
    <div>
      <h2>Không thể tải bình luận</h2>
      <p>Đã có vấn đề xảy ra khi tải phần bình luận.</p>
      <button onClick={reset}>Thử Lại</button>
      <button onClick={() => window.location.reload()}>
        Làm Mới Trang
      </button>
    </div>
  )}
>
  <CommentsSection />
</ErrorBoundary>

4. Báo Cáo Lỗi

Luôn ghi lại lỗi vào dịch vụ giám sát của bạn:

typescript Copy
<ErrorBoundary
  onError={(error, errorInfo) => {
    // Ghi lại vào dịch vụ giám sát lỗi của bạn
    errorReportingService.captureException(error, {
      extra: errorInfo,
      tags: { boundary: 'comments-section' }
    });
  }}
>
  <CommentsSection />
</ErrorBoundary>

Kiểm Tra Ranh Giới Lỗi

Tạo một thành phần để kiểm tra các tình huống lỗi:

typescript Copy
function BuggyComponent({ shouldThrowError = false }: { shouldThrowError?: boolean }) {
  const [explode, setExplode] = useState(false);

  if (shouldThrowError && explode) {
    throw new Error('Boom! 💥');
  }

  return (
    <div>
      <p>Tôi là một thành phần có thể không ổn định...</p>
      <button onClick={() => setExplode(true)}>
        Kích Hoạt Lỗi
      </button>
    </div>
  );
}

React 18 và Các Tính Năng Đồng Thời

Với các tính năng đồng thời của React 18, ranh giới lỗi hoạt động liền mạch với:

  • Ranh giới Suspense
  • Render đồng thời
  • Tự động nhóm

Hãy đảm bảo rằng ranh giới lỗi của bạn tương thích với những tính năng này bằng cách tránh các tác động phụ trong các phương thức render.

Các Phương Pháp Thay Thế và Bổ Sung

1. Xử Lý Lỗi Trong React Query

Đối với các lỗi khi lấy dữ liệu, React Query cung cấp khả năng xử lý lỗi tuyệt vời:

typescript Copy
function DataComponent() {
  const { data, error, isError } = useQuery('userData', fetchUser);

  if (isError) {
    return <div>Lỗi khi tải người dùng: {error.message}</div>;
  }

  return <div>{data?.name}</div>;
}

2. Ngữ Cảnh Lỗi

Để quản lý trạng thái lỗi toàn cục:

typescript Copy
const ErrorContext = createContext<{
  reportError: (error: Error) => void;
}>({
  reportError: () => {}
});

function ErrorProvider({ children }: { children: ReactNode }) {
  const reportError = useCallback((error: Error) => {
    // Gửi đến dịch vụ báo cáo lỗi
    console.error('Lỗi toàn cục:', error);
  }, []);

  return (
    <ErrorContext.Provider value={{ reportError }}>
      {children}
    </ErrorContext.Provider>
  );
}

Kết Luận

Ranh Giới Lỗi là những thành phần thiết yếu để xây dựng các ứng dụng React bền vững. Chúng cung cấp một mạng lưới an toàn giúp ngăn chặn toàn bộ ứng dụng bị sập do các lỗi không mong đợi, đồng thời cho bạn cơ hội để:

  • Hiển thị thông điệp lỗi có ý nghĩa cho người dùng
  • Ghi lại lỗi để sửa lỗi và giám sát
  • Cung cấp cơ chế phục hồi
  • Duy trì trải nghiệm người dùng tốt hơn

Bằng cách triển khai ranh giới lỗi một cách chiến lược trong toàn bộ ứng dụng của bạn, bạn tạo ra trải nghiệm người dùng bền vững hơn, giúp xử lý các sự cố một cách nhẹ nhàng.

Những Điểm Mấu Chốt

  1. Luôn sử dụng ranh giới lỗi trong các ứng dụng React sản xuất
  2. Đặt chúng ở vị trí chiến lược - không chỉ xung quanh toàn bộ ứng dụng của bạn
  3. Cung cấp giao diện thay thế có ý nghĩa với các tùy chọn phục hồi
  4. Ghi lại lỗi vào dịch vụ giám sát của bạn để sửa lỗi
  5. Kiểm tra các tình huống lỗi trong quá trình phát triển
  6. Kết hợp với các mô hình xử lý lỗi khác để có độ bao phủ toàn diện

Nhớ rằng: Ranh Giới Lỗi không phải là để ngăn chặn lỗi, mà là để xử lý một cách duyên dáng. Hãy tập trung vào việc cung cấp trải nghiệm người dùng tốt nhất có thể khi mọi thứ không diễn ra như mong đợi.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào