ErrorBoundaries, Hooks & Class based components

2021, May 26    

Hooks are great, and used in conjunction with functional components it can make your frontend codebase very clean and efficient.

However, if you try to use one in an older, class based component (the type that usually read

class myClass extends React.Component

you will get an error which reads something like

Invalid hook call. Hooks can only be called inside of the body of a function component.

Usually you shouldn’t even hit this issue, as in 99% of cases anything you can do with a class based component you can do with a functional one and you will either be working with one or the other.

There are a few apis that can not be used in a functional component, and I hit this recently when trying to implement Error Boundaries

This  relies on a static getDerivedStateFromError method which is part of the Component class and cannot be used in a functional component. There are plans for this pattern to be implemented into something that can be consumed by a functional component, but for now this needs a work around.

I opted to use a higher order component for this and simply pass the hook down as a prop.

my functional component (with hook) looks like this

function functionalComponent(Component) {
  return function WrappedComponent(props) {
    const customHook = useCustomHook();
    return <Component {...props} customHook={customHook} showDialog={showDialog} />;
  }
}

and my ErrorBoundry (or other class based component) can look like this

class ErrorBoundary extends Component {
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    //Error! perhaps do something with this.props.customHook which has been passed in
  }

  render() {
    const showDialog = this.props.showDialog; //also passed in
    if (showDialog) {
      return <SystemError />;
    }
    return this.props.children;
  }
}

ErrorBoundary.propTypes = {
  children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
};

export default functionalComponent(ErrorBoundary);

And then in the main body of the app you can have something like

<ErrorBoundry>
    <ChildrenHere />
</ErrorBoundry

This keeps the architecture of the app clean and maintainable, and when error boundaries get implemented as a hook on it’s own, the above can be easily refactored out and replaced with the fresh bits.