diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..b921b50
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/ErrorBoundary.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+// 🧑🏻💻 1.A Setup the following types
+// Props with fallback & children values both are React.ReactNode | React.ReactNode[]
+// State with hasError: boolean
+type Props = {
+ fallback: React.ReactNode;
+ children: React.ReactNode;
+};
+
+type State = {
+ hasError: boolean;
+};
+
+// 🧑🏻💻 1.B Create a class component called ErrorBoundary which extends the React.Component interface. The params the interface will take are
+// Ew why? It's because functional components do not have all the life cycle methods you need where as a class does.
+// https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
+
+export class ErrorBoundary extends React.Component {
+ state: State = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: unknown, info: unknown) {
+ console.error('Error caught by boundary:', error, info);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx
new file mode 100644
index 0000000..7ce8117
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/components/Fallback.tsx
@@ -0,0 +1,29 @@
+import { PokemonBackground } from '@shared/components/PokemonBackground/PokemonBackground';
+
+export const Fallback = () => {
+ return (
+
+
+
+
+
+ Opps, look like this one got away from us!
+
+
+
+ We will make sure to try and catch it next time.
+
+
+ Please try again
+
+
+
+ );
+};
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..86a013f
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Final } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/🥇 Gold/Error Boundaries/02-Exercise',
+ component: Final
+};
+
+export default meta;
+type Story = StoryObj;
+
+/*
+ * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
+ * to learn more about using the canvasElement to query the DOM
+ */
+export const Default: Story = {
+ play: async () => {},
+ args: {}
+};
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx
new file mode 100644
index 0000000..4846d4d
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/exercise/exercise.tsx
@@ -0,0 +1,83 @@
+import { PokemonBackground } from '@shared/components/PokemonBackground/PokemonBackground';
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonCardsApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+
+/**
+ * Exercise: Add an error boundary
+ *
+ * 🤔 Observations of this file
+ * So it's clear that line 92 is the error. A developer has added a ts-comment to ignore the problem but what we want to do first is make an error boundary so we know that we have caught the error incase this happens again.
+ *
+ * You may notice there already is a components/Feedback.tsx made and that was made purely on the basis that it's not the focus of the course to style things. We will be using that later.
+ *
+ * We need to tackle this two stages:
+ *
+ * Stage one - Create the error boundary in components/ErrorBoundary.tsx
+ * Stage two - Apply the ErrorBoundary.
+ *
+ */
+
+export const Final = () => {
+ const { data, isLoading, isError } = usePokedex<
+ TPokemonCardsApiResponse[]
+ >({
+ path: 'cards',
+ queryParams: 'pageSize=24&q=types:fire&supertype:pokemon'
+ });
+
+ if (isError) {
+ return (
+
+ Holy smokes!
+
+ It looks like Team Rocket has captured the fire pokemon!
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ {[...new Array(12)].map((_, index) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+ Fire Pokemon
+
+
+ {data &&
+ data.length > 0 &&
+ data.map((pokemon) => (
+
+

+
+ ))}
+
+
+ );
+};
+
+// export const Final = () => (
+// }>
+//
+//
+// );
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..4d6f49f
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/ErrorBoundary.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+type Props = {
+ fallback: React.ReactNode;
+ children: React.ReactNode;
+};
+
+type State = {
+ hasError: boolean;
+};
+
+export class ErrorBoundary extends React.Component {
+ state: State = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: unknown, info: unknown) {
+ console.error('Error caught by boundary:', error, info);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx
new file mode 100644
index 0000000..7ce8117
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/final/components/Fallback.tsx
@@ -0,0 +1,29 @@
+import { PokemonBackground } from '@shared/components/PokemonBackground/PokemonBackground';
+
+export const Fallback = () => {
+ return (
+
+
+
+
+
+ Opps, look like this one got away from us!
+
+
+
+ We will make sure to try and catch it next time.
+
+
+ Please try again
+
+
+
+ );
+};
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.stories.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.stories.tsx
new file mode 100644
index 0000000..e878de1
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Final } from './final';
+
+const meta: Meta = {
+ title: 'Lessons/🥇 Gold/Error Boundaries/03-Final',
+ component: Final
+};
+
+export default meta;
+type Story = StoryObj;
+
+/*
+ * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
+ * to learn more about using the canvasElement to query the DOM
+ */
+export const Default: Story = {
+ play: async () => {},
+ args: {}
+};
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.tsx b/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.tsx
new file mode 100644
index 0000000..8c207a0
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/final/final.tsx
@@ -0,0 +1,70 @@
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonCardsApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+import { ErrorBoundary } from './components/ErrorBoundary';
+import { Fallback } from './components/Fallback';
+import { PokemonBackground } from '@shared/components/PokemonBackground/PokemonBackground';
+
+const Screen = () => {
+ const { data, isLoading, isError } = usePokedex<
+ TPokemonCardsApiResponse[]
+ >({
+ path: 'cards',
+ queryParams: 'pageSize=24&q=types:fire&supertype:pokemon'
+ });
+
+ if (isError) {
+ return (
+
+ Holy smokes!
+
+ It looks like Team Rocket has captured the fire pokemon!
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ {[...new Array(12)].map((_, index) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+ Fire Pokemon
+
+
+ {data &&
+ data.length > 0 &&
+ data.map((pokemon) => (
+
+

+
+ ))}
+
+
+ );
+};
+
+export const Final = () => (
+ }>
+
+
+);
diff --git a/src/course/02- lessons/03-Gold/ErrorBoundary/lesson.mdx b/src/course/02- lessons/03-Gold/ErrorBoundary/lesson.mdx
new file mode 100644
index 0000000..45013f5
--- /dev/null
+++ b/src/course/02- lessons/03-Gold/ErrorBoundary/lesson.mdx
@@ -0,0 +1,113 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# Error Boundaries Pattern
+
+React makes it easy to build interactive UIs, but what happens when something goes wrong during rendering? Without safeguards, an error in one part of the UI can crash the entire application.
+
+That’s where **error boundaries** come in.
+
+## What are Error Boundaries?
+
+Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
+
+They catch errors during:
+
+- Rendering
+- Lifecycle methods
+- Constructors of the whole tree below them
+
+They **do not** catch:
+
+- Event handlers (you must use try/catch in those)
+- Asynchronous code (like **setTimeout**)
+- Server-side rendering errors
+
+## Example Use Case
+
+Imagine you’re displaying a user profile, and something breaks inside that component. Without an error boundary, your entire page might go blank. With an error boundary, you can show a friendly error message and allow the rest of the app to keep functioning.
+
+## Implementation
+
+Here’s a simple error boundary component in React:
+
+```tsx
+import React from 'react';
+
+type Props = {
+ fallback: React.ReactNode;
+ children: React.ReactNode;
+};
+
+type State = {
+ hasError: boolean;
+};
+
+export class ErrorBoundary extends React.Component {
+ state: State = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: unknown, info: unknown) {
+ console.error('Error caught by boundary:', error, info);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+
+ return this.props.children;
+ }
+}
+```
+
+And using it:
+
+```JSX
+Something went wrong.
}>
+
+
+```
+
+> Alternatively you can use [React Error Boundary](https://www.npmjs.com/package/react-error-boundary) which takes care of all the scenarios needed for your error boundary in both React DOM & React Native.
+
+## Why use this pattern?
+
+Error boundaries:
+
+- Improve user experience by preventing total app crashes
+- Provide a controlled fallback UI
+- Help developers catch and log production errors
+- Allow isolating risky components
+
+Here’s a quick visual guide:
+
+- Without Error Boundary With Error Boundary
+- App crashes completely App renders fallback component
+- Debugging is harder Error is logged and isolated
+
+## Exercise
+
+### Scenario
+
+You work in a company that's building a new feature where it displays all fire type Pokemon from the pokedex. A bug was introduced in the staging environment which crashed the application. This raised questions into how we should handle these situations better and log them while providing a better user experience to the customer. We are tasked to build a generic ErrorBoundary which will capture app crashes in our application.
+
+### What are we going to do?
+
+In today’s exercise, we’re going to build a reusable ErrorBoundary component to isolate failures.
+
+It should:
+
+- Catch rendering errors in any child component.
+- Display a custom fallback UI when an error occurs.
+- Log the error using console.error() where we can see it in dev tools.
+
+## Feedback
+
+Your feedback helps improve this course. If you have suggestions or spot anything confusing, please take a moment to leave feedback:
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/shared/components/PokemonBackground/PokemonBackground.tsx b/src/shared/components/PokemonBackground/PokemonBackground.tsx
new file mode 100644
index 0000000..8282743
--- /dev/null
+++ b/src/shared/components/PokemonBackground/PokemonBackground.tsx
@@ -0,0 +1,21 @@
+import classNames from 'classnames';
+
+export const PokemonBackground = ({
+ children,
+ bodyClassName
+}: {
+ children: React.ReactNode | React.ReactNode[];
+ bodyClassName?: string;
+}) => (
+
+
+
+
+ {children}
+
+
+);
diff --git a/src/shared/hooks/usePokedex.ts b/src/shared/hooks/usePokedex.ts
index 849aea4..11035e3 100644
--- a/src/shared/hooks/usePokedex.ts
+++ b/src/shared/hooks/usePokedex.ts
@@ -16,12 +16,14 @@ type TTypesApi = {
path: 'types';
skip?: boolean;
queryParams?: string;
+ fail?: boolean;
};
type TCardsApi = {
path: 'cards';
queryParams?: string;
skip?: boolean;
+ fail?: boolean;
};
interface IUsePokedexState {
@@ -50,7 +52,8 @@ const usePokedexReducer = (
export const usePokedex = ({
path,
queryParams = '',
- skip = false
+ skip = false,
+ fail = false
}: TCardsApi | TTypesApi): IUsePokedexState => {
const [state, dispatch] = useReducer(usePokedexReducer, {
isError: false,
@@ -74,6 +77,10 @@ export const usePokedex = ({
const json = await response.json();
+ if (fail) {
+ throw new Error('Error');
+ }
+
dispatch({ type: 'SUCCESS', payload: json.data });
} catch (e) {
dispatch({ type: 'ERROR' });