Skip to content

Commit

Permalink
feat: CE-544 - create error boundary component (#344)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike Sears <[email protected]>
Co-authored-by: Barrett Falk <[email protected]>
  • Loading branch information
3 people authored Apr 29, 2024
1 parent cfdffe5 commit 3e9834c
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 53 deletions.
93 changes: 48 additions & 45 deletions frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import COMPLAINT_TYPES from "./types/app/complaint-types";
import { getCodeTableVersion, getConfigurations, getOfficerDefaultZone } from "./store/reducers/app";
import { CreateComplaint } from "./components/containers/complaints/details/complaint-details-create";
import { UserManagement } from "./components/containers/admin/user-management";
import GenericErrorBoundary from "./components/error-handling/generic-error-boundary";

const App: FC = () => {
const dispatch = useAppDispatch();
Expand All @@ -31,57 +32,59 @@ const App: FC = () => {
}, [dispatch]);

return (
<Router>
<ScrollToTop />
<Modal />
<PageLoader />
<Routes>
<Route element={<ProtectedRoutes roles={[Roles.COS_ADMINISTRATOR]} />}>
<GenericErrorBoundary>
<Router>
<ScrollToTop />
<Modal />
<PageLoader />
<Routes>
<Route element={<ProtectedRoutes roles={[Roles.COS_ADMINISTRATOR]} />}>
<Route
path="/"
element={<ComplaintsRouteWrapper />}
/>
<Route
path="/complaints/:type?"
element={<ComplaintsRouteWrapper />}
/>
<Route
path="/complaint/:complaintType/:id"
element={<ComplaintDetailsEdit />}
/>
<Route
path="/zone/at-a-glance"
element={<ZoneAtAGlance />}
/>
<Route
path="/complaint/createComplaint"
element={<CreateComplaint />}
/>
</Route>
<Route element={<ProtectedRoutes roles={[Roles.TEMPORARY_TEST_ADMIN]} />}>
<Route
path="/admin/user"
element={<UserManagement />}
/>
</Route>
<Route
path="/"
element={<ComplaintsRouteWrapper />}
path="/not-authorized"
element={<NotAuthorized />}
/>
<Route
path="/complaints/:type?"
element={<ComplaintsRouteWrapper />}
path="*"
element={<NotFound />}
/>
<Route
path="/complaint/:complaintType/:id"
element={<ComplaintDetailsEdit />}
path="/reference"
element={
<>
<ColorReference /> <MiscReference /> <SpaceReference />
</>
}
/>
<Route
path="/zone/at-a-glance"
element={<ZoneAtAGlance />}
/>
<Route
path="/complaint/createComplaint"
element={<CreateComplaint />}
/>
</Route>
<Route element={<ProtectedRoutes roles={[Roles.TEMPORARY_TEST_ADMIN]} />}>
<Route
path="/admin/user"
element={<UserManagement />}
/>
</Route>
<Route
path="/not-authorized"
element={<NotAuthorized />}
/>
<Route
path="*"
element={<NotFound />}
/>
<Route
path="/reference"
element={
<>
<ColorReference /> <MiscReference /> <SpaceReference />
</>
}
/>
</Routes>
</Router>
</Routes>
</Router>
</GenericErrorBoundary>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { FC, ReactNode } from "react";
import { useErrorBoundary } from "../../hooks/error-boundary";
import logo from "../../../assets/images/branding/CE-Temp-Logo.svg";
import { Footer } from "../containers/layout";
import { TbFaceIdError } from "react-icons/tb";

type props = {
children?: ReactNode;
};

const GenericErrorBoundary: FC<props> = ({ children }) => {
const [error] = useErrorBoundary();

if (error) {
return (
<div className="comp-container fixed-header">
{/* <!-- --> */}

<div className="comp-header">
<div className="comp-header-logo comp-nav-item-icon-inverted">
<img
className="logo-src"
src={logo}
alt="logo"
/>
</div>

<div className="comp-header-content">
<div className="comp-header-left">{/* <!-- future left hand content --> */}</div>
<div className="comp-header-right">
<div className="header-btn-lg pr-0">
<div className="widget-content p-0">
<div className="widget-content-wrapper">
<div className="widget-content-left">{/* <!-- search --> */}</div>
<div className="widget-content-left"></div>
<div className="widget-content-right">
{/* <!-- --> */}

{/* <!-- --> */}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* <!-- --> */}

<div className="error-container">
<div className="message">
<TbFaceIdError />
{/* <br /> */}
<h1 className="comp-padding-top-25">System error</h1>Please refresh the page to try again. If you still have
problems, contact the Compliance & Enforcement Digital Services team at{" "}
<a href="mailto:[email protected]">[email protected]</a>
</div>
</div>

<Footer />
</div>
);
}

return <>{children}</>;
};

export default GenericErrorBoundary;
125 changes: 125 additions & 0 deletions frontend/src/app/hooks/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, {
Component,
useState,
useCallback,
createContext,
useContext,
MutableRefObject,
useMemo,
useRef,
ComponentType,
ReactNode,
PropsWithChildren,
ReactElement,
ErrorInfo,
} from "react";

type ComponentDidCatch = (error: Error, errorInfo: ErrorInfo) => void;

interface ErrorBoundaryProps {
error: Error | undefined;
onError: ComponentDidCatch;
}

class ErrorBoundary extends Component<PropsWithChildren<ErrorBoundaryProps>> {
displayName = "UseErrorBoundary";

componentDidCatch(...args: Parameters<NonNullable<Component["componentDidCatch"]>>) {
// silence React warning:
// ErrorBoundary: Error boundaries should implement getDerivedStateFromError().
// In that method, return a state update to display an error message or fallback UI
this.setState({});
this.props.onError(...args);
}

render() {
return this.props.children;
}
}

const noop = () => false;

interface ErrorBoundaryCtx {
componentDidCatch: MutableRefObject<ComponentDidCatch | undefined>;
error: Error | undefined;
errorInfo: ErrorInfo | undefined;
setError: (error: Error | undefined) => void;
}

const errorBoundaryContext = createContext<ErrorBoundaryCtx>({
componentDidCatch: { current: undefined },
error: undefined,
errorInfo: undefined,
setError: noop,
});

// eslint-disable-next-line @typescript-eslint/ban-types
export function ErrorBoundaryContext({ children }: { children?: ReactNode }) {
const [error, setError] = useState<Error>();
const [errorInfo, setErrorInfo] = useState<ErrorInfo>();

const componentDidCatch = useRef<ComponentDidCatch>();
const ctx = useMemo(
() => ({
componentDidCatch,
error,
errorInfo,
setError,
}),
[error, errorInfo],
);
return (
<errorBoundaryContext.Provider value={ctx}>
<ErrorBoundary
error={error}
onError={(error, errorInfo) => {
setError(error);
setErrorInfo(errorInfo);
componentDidCatch.current?.(error, errorInfo);
}}
>
{children}
</ErrorBoundary>
</errorBoundaryContext.Provider>
);
}
ErrorBoundaryContext.displayName = "ErrorBoundaryContext";

export function withErrorBoundary<Props = Record<string, unknown>>(
WrappedComponent: ComponentType<Props>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): (props: PropsWithChildren<Props>) => ReactElement<any, any> {
function WithErrorBoundary(props: Props) {
return (
<ErrorBoundaryContext>
<WrappedComponent
key="WrappedComponent"
{...props}
/>
</ErrorBoundaryContext>
);
}
WithErrorBoundary.displayName = `WithErrorBoundary(${
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"
})`;

return WithErrorBoundary;
}

export type UseErrorBoundaryReturn = [
error: Error | undefined,
errorInfo: ErrorInfo | undefined,
resetError: () => void,
];

export function useErrorBoundary(componentDidCatch?: ComponentDidCatch): UseErrorBoundaryReturn {
const ctx = useContext(errorBoundaryContext);
ctx.componentDidCatch.current = componentDidCatch;
const resetError = useCallback(() => {
ctx.setError(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return [ctx.error, ctx.errorInfo, resetError];
}
2 changes: 2 additions & 0 deletions frontend/src/assets/sass/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@
@import "./maps.scss";
@import "./carousel.scss";
@import "./form.scss";
@import "./errors.scss";

19 changes: 19 additions & 0 deletions frontend/src/assets/sass/errors.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.error-container {
margin-top: auto;
position: relative;

.message {
position: absolute;
top: 50%;
left: 50%;
width: 625px;
transform: translateX(-50%);

svg {
height: 175px;
width: 175px;
float: left;
padding-right: 25px;
}
}
}
19 changes: 11 additions & 8 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@ import "./assets/sass/app.scss";

import reportWebVitals from "./reportWebVitals";
import { PersistGate } from "redux-persist/integration/react";
import { ErrorBoundaryContext } from "./app/hooks/error-boundary";

const container = document.getElementById("root")!;
const root = createRoot(container);

const onAuthenticatedCallback = () =>
root.render(
<StrictMode>
<Provider store={store}>
<PersistGate
loading={null}
persistor={persistor}
>
<App />
</PersistGate>
</Provider>
<ErrorBoundaryContext>
<Provider store={store}>
<PersistGate
loading={null}
persistor={persistor}
>
<App />
</PersistGate>
</Provider>
</ErrorBoundaryContext>
</StrictMode>,
);

Expand Down

0 comments on commit 3e9834c

Please sign in to comment.