How to properly type a React ErrorBoundary class component in Typescript?
S

3

21

Here is my current attempt on how to properly type a React ErrorBoundary class component in Typescript:

import React from "react";
import ErrorPage from "./ErrorPage";
import type { Store } from "redux";   // I'M PASSING THE REDUX STORE AS A CUSTOM PROP

interface Props {
  store: Store         // THIS IS A CUSTOM PROP THAT I'M PASSING
}

interface State {      // IS THIS THE CORRECT TYPE FOR THE state ?
  hasError: boolean
}

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

  static getDerivedStateFromError(error): State {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo: React.ErrorInfo): void {
    // You can also log the error to an error reporting service
    // logErrorToMyService(error, errorInfo);
    console.log("error: " + error);
    console.log("errorInfo: " + JSON.stringify(errorInfo));
    console.log("componentStack: " + errorInfo.componentStack);
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return(
        <ErrorPage
          message={"Something went wrong..."}
        />
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

This question is about the types for this ErrorBoundary class component. I'm breaking it into parts to make it easier.


PART A: Types for props and state

Is what I'm doing right?

interface Props {
  store: Store         // THIS IS A CUSTOM PROP THAT I'M PASSING
}

interface State {      // IS THIS THE CORRECT TYPE FOR THE state ?
  hasError: boolean
}

class ErrorBoundary extends React.Component<Props,State> {}

PART B: getDerivedStateFromError(error)

What type should I choose for the error parameter? The return type should be the State type, right?

static getDerivedStateFromError(error): State {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

PART C: componentDidCatch(error, errorInfo: React.React.ErrorInfo)

What type should I choose for the error parameter? For the errorInfo, React does have a ErrorInfo type that seems to be correct. Is it? The return type should be void, correct?

componentDidCatch(error, errorInfo: React.ErrorInfo): void {
    console.log("error: " + error);
    console.log("errorInfo: " + JSON.stringify(errorInfo));
    console.log("componentStack: " + errorInfo.componentStack);
}

PART D: the render() method

Should the return type be ReactNode ?

render(): React.ReactNode {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return(
        <ErrorPage
          message={"Something went wrong..."}
        />
      );
    }

    return this.props.children;
  }
Stilbite answered 16/9, 2020 at 9:17 Comment(5)
Is this syntax: import type { Store } from "redux"; allowed?Thoroughwort
@captain-yossarian yes, it seems to be working just fine. But the import type syntax was release in a more recent version of Typescript, not sure which one, but it won't work in some older versions.Stilbite
You should pass redux store to <Provider store={store}> and not directly to your ComponentThoroughwort
I'm doing that also. But not for the ErrorBoundary. But I guess I can access the store with useStore() from react-redux since the ErrorBoundary will be inside the <Provider store={store}>. Thanks. EDIT: actually I can't use useStore inside a class component. Just found that out. Got that error message: React Hook "useStore" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.Stilbite
PART C: This member must have an 'override' modifier because it overrides a member in the base class 'Component<Record<string, never>, ErrorBoundaryState, any>'.ts(4114) PART D: This member must have an 'override' modifier because it overrides a member in the base class 'Component<Record<string, never>, ErrorBoundaryState, any>'.ts(4114)Fiducial
P
5

You will get all your answers in the file index.d.ts from @types/react. If you are using an IDE like VSCode, you can Ctrl+click on a type to directly go to its definition.

Here is the precise answers to your questions, but before, let me advise you to prefer using react hooks instead of classes.


EDIT by OP: I never use class components, always prefer function and hooks, but from React docs on Error Boundaries:

Error boundaries work like a JavaScript catch {} block, but for components. Only class components can be error boundaries.


The lines I give are the one from index.d.ts in version 16.9.49.

Part A: yes, that's the way to do.

Part B: as you can see at line 658, error is of type any and the return type is a partial of the state or null.

Part C: at line 641, you will see that error is of type Error and return type is void.

Part D: at line 494, you can see that render should return a ReactNode

Photochronograph answered 16/9, 2020 at 9:33 Comment(1)
Thanks, man. I was just looking into the type definitions file right now. I forgot that those methods signatures would be inside the type definitions file. I decided to use unknown for the error parameter in getDerivedStateFromError(error), instead of any.Stilbite
R
23

You can use the following code as an Error Boundary:

import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(_: Error): State {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("Uncaught error:", error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return <h1>Sorry.. there was an error</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;
Reich answered 9/2, 2022 at 7:18 Comment(0)
P
5

You will get all your answers in the file index.d.ts from @types/react. If you are using an IDE like VSCode, you can Ctrl+click on a type to directly go to its definition.

Here is the precise answers to your questions, but before, let me advise you to prefer using react hooks instead of classes.


EDIT by OP: I never use class components, always prefer function and hooks, but from React docs on Error Boundaries:

Error boundaries work like a JavaScript catch {} block, but for components. Only class components can be error boundaries.


The lines I give are the one from index.d.ts in version 16.9.49.

Part A: yes, that's the way to do.

Part B: as you can see at line 658, error is of type any and the return type is a partial of the state or null.

Part C: at line 641, you will see that error is of type Error and return type is void.

Part D: at line 494, you can see that render should return a ReactNode

Photochronograph answered 16/9, 2020 at 9:33 Comment(1)
Thanks, man. I was just looking into the type definitions file right now. I forgot that those methods signatures would be inside the type definitions file. I decided to use unknown for the error parameter in getDerivedStateFromError(error), instead of any.Stilbite
R
1

You can use the exact following code and write ErrorBoundary.tsx file:

import { Component, ErrorInfo, ReactNode } from 'react';

import Spinner from './components/ui/Spinner.component';

interface ErrorBoundaryProps {
  children: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
}

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

  static getDerivedStateFromError(_: Error): ErrorBoundaryState {
    // Update state to indicate an error has occurred
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    // You can log the error to an error reporting service
    console.error('Error caught by ErrorBoundary:', error, errorInfo);
  }

  render(): ReactNode {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <>
          <Spinner />
          <div>Something went wrong.</div>
        </>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Then use it in index.tsx file:

import { ApolloProvider } from '@apollo/client';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';

import ErrorBoundary from './ErrorBoundary';
import { client } from './utils/graphql/client';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <ErrorBoundary>
      <ApolloProvider client={client}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </ApolloProvider>
    </ErrorBoundary>
  </React.StrictMode>
);

reportWebVitals();
Reich answered 9/3 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.