This
- course educates developers on best practices for writing
- React components, utilizing patterns and providing practical
- guides with real-world examples.
-
+
+ );
+};
diff --git a/src/app/content/courseData.ts b/src/app/content/courseData.ts
new file mode 100644
index 0000000..45a0c2c
--- /dev/null
+++ b/src/app/content/courseData.ts
@@ -0,0 +1,83 @@
+export const courseLevels = [
+ {
+ title: 'Bronze Level',
+ subtitle: 'Fundamentals',
+ emoji: '๐ฅ',
+ color: 'from-amber-500 to-orange-600',
+ bgColor: 'bg-amber-50',
+ borderColor: 'border-amber-200',
+ patterns: [
+ 'Conditional Rendering',
+ 'Props Combination',
+ 'React Hooks',
+ 'Presentational & Container',
+ 'Slots Pattern'
+ ]
+ },
+ {
+ title: 'Silver Level',
+ subtitle: 'Intermediate',
+ emoji: '๐ฅ',
+ color: 'from-slate-400 to-slate-600',
+ bgColor: 'bg-slate-50',
+ borderColor: 'border-slate-200',
+ patterns: [
+ 'Compound Components',
+ 'Controlled Components',
+ 'Uncontrolled Components',
+ 'FACC Pattern',
+ 'Render Children',
+ 'Render Props',
+ 'Provider Pattern',
+ 'State Reducer',
+ 'Portals',
+ 'Polymorphic Components'
+ ]
+ },
+ {
+ title: 'Gold Level',
+ subtitle: 'Advanced',
+ emoji: '๐ฅ',
+ color: 'from-yellow-400 to-yellow-600',
+ bgColor: 'bg-yellow-50',
+ borderColor: 'border-yellow-200',
+ patterns: [
+ 'Higher Order Components',
+ 'Suspense & Lazy Loading',
+ 'Headless Components'
+ ]
+ }
+];
+
+export const features = [
+ {
+ icon: '๐ฎ',
+ title: 'Pokemon-Themed Learning',
+ description: 'Learn React patterns through engaging Pokemon examples that make complex concepts memorable and fun.'
+ },
+ {
+ icon: '๐',
+ title: 'Interactive Storybook',
+ description: 'Hands-on exercises with live code examples. See patterns in action and experiment with real implementations.'
+ },
+ {
+ icon: '๐ฏ',
+ title: 'Progressive Difficulty',
+ description: 'Start with fundamentals and advance to expert-level patterns. Each lesson builds on previous knowledge.'
+ },
+ {
+ icon: '๐ง',
+ title: 'Real-World Applications',
+ description: 'Learn when and how to apply each pattern with practical guidance for production applications.'
+ },
+ {
+ icon: 'โก',
+ title: 'Modern React 19',
+ description: 'Built with the latest React features including hooks, Suspense, and concurrent rendering patterns.'
+ },
+ {
+ icon: '๐จ',
+ title: 'Beautiful UI Examples',
+ description: 'Styled with Tailwind CSS to show how patterns work in polished, production-ready interfaces.'
+ }
+];
\ No newline at end of file
diff --git a/src/app/content/index.ts b/src/app/content/index.ts
new file mode 100644
index 0000000..03e0d48
--- /dev/null
+++ b/src/app/content/index.ts
@@ -0,0 +1 @@
+export { courseLevels, features } from './courseData';
\ No newline at end of file
diff --git a/src/app/icons/DotPattern.tsx b/src/app/icons/DotPattern.tsx
new file mode 100644
index 0000000..95c48aa
--- /dev/null
+++ b/src/app/icons/DotPattern.tsx
@@ -0,0 +1,9 @@
+export const DotPattern = () => (
+
+);
\ No newline at end of file
diff --git a/src/app/icons/index.ts b/src/app/icons/index.ts
new file mode 100644
index 0000000..a9faee5
--- /dev/null
+++ b/src/app/icons/index.ts
@@ -0,0 +1 @@
+export { DotPattern } from './DotPattern';
\ No newline at end of file
diff --git a/src/course/01-introduction/01-Welcome.mdx b/src/course/01-introduction/01-Welcome.mdx
index fe55e20..10c2523 100644
--- a/src/course/01-introduction/01-Welcome.mdx
+++ b/src/course/01-introduction/01-Welcome.mdx
@@ -34,22 +34,36 @@ I will be going in high detail on each of the lessons so if there are some holes
## Contents
-Each lesson is broken down in an exercise file and a final file. The exercise file will have instructions in pseudo format to help guide you through the code snippets. Not to worry though, there will be videos to support you along the way.
+Each lesson is broken down in an exercise file and a final file. The exercise file will have instructions in pseudo format to help guide you through the code snippets.
### Lessons
-- 01 - [Conditionally rendering pattern](?path=/docs/lessons-01-conditional-rendering-pattern-01-lesson--docs)
-- 02 - [Props combination pattern](?path=/docs/lessons-02-props-combination-pattern-01-lesson--docs)
-- 03 - [Render props pattern](?path=/docs/lessons-03-render-props-pattern-01-lesson--docs)
-- 04 - [Presentational and container components pattern](?path=/docs/lessons-04-presentational-container-pattern-01-lesson--docs)
-- 05 - [React Hooks pattern](?path=/docs/lessons-05-hooks-pattern-01-lesson--docs)
-- 06 - [Controlled component pattern](?path=/docs/lessons-06-controlled-components-pattern-01-lesson--docs)
-- 07 - [Higher order component](?path=/docs/lessons-07-higher-order-components-pattern-01-lesson--docs)
-- 08 - [The Provider pattern](?path=/docs/lessons-08-provider-pattern-01-lesson--docs)
-- 09 - [The State Reducer pattern](?path=/docs/lessons-09-state-reducer-pattern-01-lesson--docs)
-- 10 - [Compound components pattern](?path=/docs/lessons-10-compound-components-pattern-01-lesson--docs)
-- 11 - [Slots pattern](?path=/docs/lessons-11-slots-01-lesson--docs)
-- 12 - [Portals pattern](?path=/docs/lessons-12-portals-01-lesson--docs)
+#### ๐ฅ Bronze
+
+- [Conditionally rendering pattern](?path=/docs/lessons-๐ฅ-bronze-conditional-rendering-pattern-01-lesson--docs)
+- [Props combination pattern](?path=/docs/lessons-๐ฅ-bronze-props-combination-pattern-01-lesson--docs)
+- [React Hooks pattern](?path=/docs/lessons-๐ฅ-bronze-hooks-pattern-01-lesson--docs)
+- [Presentational and container components pattern](?path=/docs/lessons-๐ฅ-bronze-presentational-container-pattern-01-lesson--docs)
+- [Slots pattern](?path=/docs/lessons-๐ฅ-bronze-slots-01-lesson--docs)
+
+#### ๐ฅ Silver
+
+- [Compound components pattern](?path=/docs/lessons-๐ฅ-Silver-compound-components-pattern-01-lesson--docs)
+- [Controlled component pattern](?path=/docs/lessons-๐ฅ-Silver-controlled-components-pattern-01-lesson--docs)
+- [Uncontrolled components pattern](?path=/docs/lessons-๐ฅ-silver-uncontrolled-components-01-lesson--docs)
+- [FACC pattern](?path=/docs/lessons-๐ฅ-silver-facc-pattern-01-lesson--docs)
+- [Render children pattern](?path=/docs/lessons-๐ฅ-silver-render-children-pattern-01-lesson--docs)
+- [Render props pattern](?path=/docs/lessons-๐ฅ-Silver-render-props-pattern-01-lesson--docs)
+- [The Provider pattern](?path=/docs/lessons-๐ฅ-Silver-provider-pattern-01-lesson--docs)
+- [The State Reducer pattern](?path=/docs/lessons-๐ฅ-Silver-state-reducer-pattern-01-lesson--docs)
+- [Portals pattern](?path=/docs/lessons-๐ฅ-Silver-portals-01-lesson--docs)
+- [Polymorphic components pattern](?path=/docs/lessons-๐ฅ-silver-polymorphic-components-01-lesson--docs)
+
+#### ๐ฅ Gold
+
+- [Higher order component](?path=/docs/lessons-๐ฅ-gold-higher-order-components-pattern-01-lesson--docs)
+- [Suspense & lazy loading pattern](?path=/docs/lessons-๐ฅ-gold-suspense-lazy-loading-01-lesson--docs)
+- [Headless components pattern](?path=/docs/lessons-๐ฅ-gold-headless-components-01-lesson--docs)
## FAQs
diff --git a/src/course/01-introduction/02-GettingStarted.mdx b/src/course/01-introduction/02-GettingStarted.mdx
index 2e084b0..966de2a 100644
--- a/src/course/01-introduction/02-GettingStarted.mdx
+++ b/src/course/01-introduction/02-GettingStarted.mdx
@@ -4,7 +4,7 @@ import { Meta } from '@storybook/blocks';
# Getting Started
-> Node version 18 required.
+> Node version 18+ required.
## Installation
diff --git a/src/course/01-introduction/03-LessonStructure.mdx b/src/course/01-introduction/03-LessonStructure.mdx
index 8626b76..43fa999 100644
--- a/src/course/01-introduction/03-LessonStructure.mdx
+++ b/src/course/01-introduction/03-LessonStructure.mdx
@@ -4,7 +4,11 @@ import { Meta } from '@storybook/blocks';
# Lesson Structure
-As you will have already noticed in the sidebar that there is a "Lessons" section. Each lesson will get slightly more and more complex as we go on so it eases us into the course. Each lesson will also contain an "exercise.(tsx)" file and a "final.(tsx)".
+As you will have already noticed in the sidebar that there is a "Lessons" section. Each lesson sits within a Bronze/Silver/Gold tier folder which mirrors to the complexity of that pattern & we provide more challenging exercises. Each lesson will also contain an "exercise" folder and a "final" folder.
+
+## Storybook / Folder Structure
+
+As you can see the storybook sidebar mirrors the way the folder structure is within the repo. This is done so you can easily navigate to the files you are changing within the exercises.
## Exercise files
@@ -60,6 +64,4 @@ const Component = () => {
## Final files
-If you get stuck do not worry! Each lesson.mdx file will have a video going through it all and there will be a final.tsx file showing the final solution of each exercise.
-
-[Let's get started](?path=/docs/lessons-01-conditional-rendering-pattern-01-lesson--docs)
+If you get stuck do not worry! Each will have a final folder in the lesson showing the final solution of each exercise. Head over to any of the lessons to get started with which patterns you wish to learn.
diff --git a/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx b/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx
deleted file mode 100644
index 5c66860..0000000
--- a/src/course/02- lessons/01-ConditionalRendering/exercise.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react";
-
-import { userEvent, within, expect } from "@storybook/test";
-
-import { ComponentOne } from "./exercise";
-
-const meta: Meta = {
- title: "Lessons/01 - Conditional Rendering Pattern/02-Exercise",
- component: ComponentOne,
-};
-
-export default meta;
-type Story = StoryObj;
-
-const username = "John Doe";
-
-/*
- * 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 ({ canvasElement }) => {
- const canvas = within(canvasElement);
-
- await userEvent.click(canvas.getByRole("button", { name: "Login" }));
-
- await expect(canvas.getByText(`Welcome ${username}`)).toBeInTheDocument();
- await expect(canvas.queryByRole("button", { name: "Login" })).toBeNull();
-
- await userEvent.click(canvas.getByRole("button", { name: "Logout" }));
-
- await expect(canvas.queryByText(`Welcome ${username}`)).toBeNull();
- await expect(canvas.queryByRole("button", { name: "Logout" })).toBeNull();
- },
- args: {
- username,
- },
-};
diff --git a/src/course/02- lessons/01-ConditionalRendering/exercise.tsx b/src/course/02- lessons/01-ConditionalRendering/exercise.tsx
deleted file mode 100644
index 9fdbe6a..0000000
--- a/src/course/02- lessons/01-ConditionalRendering/exercise.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Button } from '../../../shared/components/Button/Button.component';
-
-interface IComponentProps {
- username: string;
-}
-
-// 1g - ๐ฃ The ignore lint rules can be removed now
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-expect-error
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const ComponentOne = (props: IComponentProps) => {
- // 1a - ๐จ๐ปโ๐ป add a useState that has false as default. Name the variable [isAuthenticated, setIsAuthenticated]
-
- // 1b - ๐จ๐ปโ๐ป create me a onLogin function which setIsAuthenticated to be true
-
- // 1c - ๐จ๐ปโ๐ป create me a onLogout function which setIsAuthenticated to be false
-
- // 1d - ๐จ๐ปโ๐ป if authenticated, return a button called "Logout" with the onClick of onLogout
- // 1e - ๐จ๐ปโ๐ป if authenticated, return some text called "Welcome {props.username}"
-
- // 1f - ๐จ๐ปโ๐ป add onClick function onLogin to the button
- return ;
-};
diff --git a/src/course/02- lessons/01-ConditionalRendering/lesson.mdx b/src/course/02- lessons/01-ConditionalRendering/lesson.mdx
deleted file mode 100644
index a27307e..0000000
--- a/src/course/02- lessons/01-ConditionalRendering/lesson.mdx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Conditional Rendering Pattern
-
-The conditional rendering pattern is a way to dynamically change UI based what values are set at that time. The most common way that this is done is by using an if statement. For example:
-
-```jsx
-const Component = (props) => {
- if (props.isAuthenticated) {
- return
Welcome {props.username}!
;
- } else {
- return
Not logged in.
;
- }
-};
-```
-
-There are many syntactical ways you can do the same as above such as the ternary:
-
-```jsx
-const Component = (props) => {
- return props.isAuthenticated ? (
-
Welcome {props.username}!
- ) : (
-
Not logged in.
- );
-};
-```
-
-Or you can use the AND syntax:
-
-```jsx
-const Component = (props) => {
- return props.isAuthenticated &&
Welcome {props.username}!
;
-};
-```
-
-If I had a lot of complexity in this component that still would be getting executed but the component will just return nothing if it's not authenticated. The best way to return something like this is to do the conditional render outside of the component, for example:
-
-```jsx
-const ComponentOne = (props) => {
- return
Welcome {props.username}
;
-};
-
-const ComponentTwo = (props) => {
- return (
-
- {/* Other components */}
- {props.isAuthenticated && }
-
- );
-};
-```
-
-### Event driven rendering
-
-There may be times when you need to conditionally render a component based on an event that has been changed. This example conditionally renders a box when you click the button.
-
-```jsx
-const Component = () => {
- const [displayBox, setDisplayBox] = useState(false);
-
- const showBox = () => {
- setDisplayBox(true);
- };
-
- const hideBox = () => {
- setDisplayBox(false);
- };
-
- return (
- <>
-
- {displayBox && (
-
-
Box
-
- )}
- >
- );
-};
-```
-
-## Exercise
-
-In the first exercise we are going to look into building a login and logout toggle which will render a username when they have logged in. Go to the exercise.tsx inside the 01-ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/02-PropsCombination/exercise.stories.tsx b/src/course/02- lessons/02-PropsCombination/exercise.stories.tsx
deleted file mode 100644
index 0b6a2f8..0000000
--- a/src/course/02- lessons/02-PropsCombination/exercise.stories.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { Exercise } from './exercise';
-
-const meta: Meta = {
- title: 'Lessons/02 - Props Combination Pattern/02-Exercise',
- component: Exercise
-};
-
-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 = {
- args: {
- title: 'The Coldest Sunset Festival',
- subText:
- 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.',
- ctaText: '#festival',
- ctaUrl: '/',
- imageAltText: 'DJ playing at a festival',
- imageUrlMobile:
- 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=767&h=640&fit=crop',
- imageUrlTablet:
- 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1024&h=640&fit=crop',
- imageUrlDesktop:
- 'https://images.unsplash.com/photo-1493676304819-0d7a8d026dcf?w=1600&h=900&fit=crop',
- containerClassName: 'containerClassName',
- titleClassName: 'titleClassName',
- subTextClassName: 'subTextClassName',
- ctaClassName: 'ctaClassName',
- imageClassName: 'imageClassName'
- }
-};
diff --git a/src/course/02- lessons/02-PropsCombination/exercise.tsx b/src/course/02- lessons/02-PropsCombination/exercise.tsx
deleted file mode 100644
index ffa858a..0000000
--- a/src/course/02- lessons/02-PropsCombination/exercise.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import classnames from 'classnames';
-
-/*
-
- 1a๐จ๐ปโ๐ป group the following together:
-
- * image - imageAltText, imageUrlMobile, imageUrlTablet, imageUrlDesktop
- * cta - ctaText, ctaUrl
- * classNames - containerClassName, titleClassName, subTextClassName, ctaClassName, imageClassName
-
-*/
-interface IExerciseProps {
- title: string;
- subText: string;
- ctaText: string;
- ctaUrl: string;
- imageAltText: string;
- imageUrlMobile: string;
- imageUrlTablet: string;
- imageUrlDesktop: string;
- containerClassName?: string;
- titleClassName?: string;
- subTextClassName?: string;
- ctaClassName?: string;
- imageClassName?: string;
-}
-
-/*
- 1b๐จ๐ปโ๐ป Update the props to match the new types defined above.
-*/
-export const Exercise = ({
- title,
- subText,
- ctaText,
- ctaUrl,
- imageAltText,
- imageUrlMobile,
- imageUrlTablet,
- imageUrlDesktop,
- containerClassName,
- titleClassName,
- subTextClassName,
- ctaClassName,
- imageClassName
-}: IExerciseProps) => {
- /*
- 2a ๐ค Could we destructure the image to be [mobile, tablet, desktop]?
- */
- /*
- 1c๐จ๐ปโ๐ป Update the props in the jsx
- */
- return (
-
-
- {/* โ๐ป picture elements are a great way to display responsive images */}
- {/* โ๐ป Using rem instead of pixels will change the image when you zoom in the page */}
- {/* โ๐ป Link: https://web.dev/learn/design/picture-element */}
-
-
-
-
-
-
-
- );
-};
diff --git a/src/course/02- lessons/02-PropsCombination/lesson.mdx b/src/course/02- lessons/02-PropsCombination/lesson.mdx
deleted file mode 100644
index 475e56a..0000000
--- a/src/course/02- lessons/02-PropsCombination/lesson.mdx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Props Combination Pattern
-
-Props are used to pass data from one component to another. The prop combination pattern groups related props into a single object. This object is then passed as a single prop to a component.
-
-Some benefits of this pattern include reduction of boilerplate code, improving code readability and maintainability.
-
-Let's take a look at an example:
-
-```jsx
-const CardComponent = ({
- title,
- subText,
- ctaText,
- ctaUrl,
- imageAltText,
- imageUrlMobile,
- imageUrlTablet,
- imageUrlDesktop,
- containerClassName,
- titleClassName,
- subTextClassName,
- ctaClassName,
- imageClassName
-}) => {/* Lots of imaginary code here... */};
-```
-
-Yes... this is pretty wild but it's very common to see this in the real world. What we have going into this component is:
-
-- image - the image sources, imageAltText
-- cta - text and ctaUrl
-- content - title, sub title
-- classNames - all classNames
-
-Now look at this when it is grouped:
-
-```jsx
-const CardComponent = ({ title, subText, cta, image }) => (
- {/* Lots of imaginary code here... */}
-);
-```
-
-It is now a lot simpler to understand, we know the component has a title, it also needs a cta and an image. If we then wanted to, we would add some styles in case we need to tweak the card.
-
-## Exercise
-
-In this exercise we are going to do the same thing that we did to the card snippet above and then we have some extras to add to it so it will get you thinking about how to change the props if necessary.
-
-Head over to the exercise file and let's begin.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/03-RenderProps/exercise.tsx b/src/course/02- lessons/03-RenderProps/exercise.tsx
deleted file mode 100644
index 5e07985..0000000
--- a/src/course/02- lessons/03-RenderProps/exercise.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { ChangeEvent, useState } from 'react';
-import { Input } from '../../../shared/components/Input/Input.component';
-import { Label } from '../../../shared/components/Label/Label.component';
-import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component';
-
-export interface ITextInputFieldProps {
- name: string;
- id: string;
- label: string;
- required?: boolean;
- errorMessage?: string;
-}
-
-/*
- * Observations
- * ๐ The current implementation uses the Controlled Component Pattern
- * The UI is already split into small components
-
- * Tasks
- * 1A ๐จ๐ปโ๐ป - Refactor the UI layer into its own component and setup the interface for its types to be:
- * hasError: boolean;
- * errorMessage?: string;
- * id: string;
- * name: string;
- * label: string;
- * input: HTMLAttributes & { required?: boolean };
- *
- * 1B ๐จ๐ปโ๐ป - Add these new types to the TextInputField
- * validate?: (value: string) => boolean;
- * children: (props: ITextFieldProps) => React.ReactNode;
- *
- * 1C ๐จ๐ปโ๐ป - Replace the return of TextInput field with the children prop we have defined.
- * ๐ You need to call children and pass down the props you need (the types above are the hint)
- *
- * 1D ๐จ๐ปโ๐ป - In the Exercise component you want to add the UI component in as children.
- * ๐ - The children should look like this {(props) => }
-*/
-
-const validateTextString = (value: string) =>
- value.trim().length === 0;
-
-export const TextInputField = ({
- name,
- label,
- id,
- required,
- errorMessage
-}: ITextInputFieldProps) => {
- const [value, setValue] = useState('');
- const [hasError, setHasError] = useState(false);
- const [isTouched, setIsTouched] = useState(false);
-
- const onChange = (event: ChangeEvent) => {
- if (required) {
- setHasError(validateTextString(event.target.value));
- }
-
- setValue(event.target.value);
- };
-
- const onFocus = () => {
- if (isTouched) {
- setHasError(false);
- }
-
- setIsTouched(true);
- };
-
- const onBlur = () => {
- if (value && validateTextString(value)) {
- setHasError(true);
- }
- };
-
- return (
-
-
-
- {errorMessage && hasError && (
-
- )}
-
- );
-};
-
-export const Exercise = () => {
- return (
-
- );
-};
diff --git a/src/course/02- lessons/03-RenderProps/lesson.mdx b/src/course/02- lessons/03-RenderProps/lesson.mdx
deleted file mode 100644
index fb3b3d9..0000000
--- a/src/course/02- lessons/03-RenderProps/lesson.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Render Props Pattern
-
-Render props is a common pattern you will see in popular NPM packages like [Formik](https://formik.org/) or [React Final form](https://final-form.org/react) and it is very useful for building components that manage the logic and pass information to their children/prop so they can use that logic in the UI layer.
-
-## With children
-
-```jsx
-const HelloWorld = ({ children }) => children({ hello: 'world' });
-
-// How you would use it.
-{({ hello }) =>
{hello}
};
-```
-
-## With props
-
-```jsx
-const HelloWorld = ({ render }) => render({ hello: 'world' });
-
-// How you would use it.
-
{hello}
} />;
-```
-
-## Exercise
-
-In this exercise we are going to create a simplfied version of the Field component which is commonly used within the libraries specified above. What we have in the code is a component which mixes three UI components (Label, TextField, ErrorMessage) with app logic which is tightly coupled together.
-
-The task: We have multiple teams that want to use that visual element for their applications but they do not align with the app logic as their apps behave differently. They all want the ability to use the UI logic alone so they can handle the app logic their way.
-
-Our task is to refactor the UI out of the Field component and then pass props into the UI component using the render props pattern.
-
-Head over to the exercise file and let's begin.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/05-Hooks/exercise.tsx b/src/course/02- lessons/05-Hooks/exercise.tsx
deleted file mode 100644
index 7191929..0000000
--- a/src/course/02- lessons/05-Hooks/exercise.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { ChangeEvent, useState } from 'react';
-import { ITextFieldProps, TextFieldComponent } from './components';
-
-/*
- * Observations
- * ๐ The current implementation uses the Render Props Pattern
- * Don't worry about the UI in the components file.
- */
-
-interface IFieldProps {
- name: string;
- validate?: (value: string) => boolean;
- required?: boolean;
-
- // 1B ๐ฃ - remove these four params and references in the function.
- id: string;
- label: string;
- errorMessage?: string;
- children: (props: ITextFieldProps) => React.ReactNode;
-}
-
-const validateTextString = (value: string) =>
- value.trim().length === 0;
-
-// 1A ๐จ๐ปโ๐ป - We need to refactor this to be called useField
-export const Field = ({
- name,
- required,
- validate,
- // 1B ๐ฃ - remove these four params and references in the function.
- label,
- id,
- errorMessage,
- children
-}: IFieldProps) => {
- const [value, setValue] = useState('');
- const [hasError, setHasError] = useState(false);
- const [isTouched, setIsTouched] = useState(false);
-
- const onChange = (event: ChangeEvent) => {
- if (required && validate) {
- setHasError(validate(event.target.value));
- }
-
- setValue(event.target.value!);
- };
-
- const onFocus = () => {
- if (isTouched) {
- setHasError(false);
- }
-
- setIsTouched(true);
- };
-
- const onBlur = () => {
- if (value && validate && validate(value)) {
- setHasError(true);
- }
- };
-
- // 1C ๐จ๐ปโ๐ป - Just return the object instead of children.
- return children({
- // 1D ๐จ๐ปโ๐ป - move name into input
- name,
- input: {
- required,
- onBlur,
- onFocus,
- onChange
- },
- hasError,
- // 1B ๐ฃ - remove these three params and references in the function.
- label,
- id,
- errorMessage
- });
-};
-
-// 2A ๐ค - What if we wanted to make multiple Fields? Our current solution would
-// require us to call useField multiple times in the same component. Let's refactor
-// what we have done into a field component which uses IFieldProps as params.
-
-export const Exercise = () => {
- // 1E ๐จ๐ปโ๐ป - call the useField and pass the { name: "input", validate: validateTextString, required: true }
- return (
-
- );
-};
diff --git a/src/course/02- lessons/05-Hooks/lesson.mdx b/src/course/02- lessons/05-Hooks/lesson.mdx
deleted file mode 100644
index a55afb9..0000000
--- a/src/course/02- lessons/05-Hooks/lesson.mdx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Hooks Pattern
-
-React Hooks, introduced in React 16.8, revolutionize the traditional presentational and container component pattern by allowing functional components to manage state and side effects directly. This means you no longer need to separate components into presentational (stateless) and container (stateful) types. Hooks like useState and useEffect enable functional components to handle both UI rendering and business logic, simplifying code and improving readability.
-
-Additionally, Hooks enhance reusability and reduce prop drilling. Custom Hooks allow you to extract and share logic across components, promoting a more modular codebase. With features like useContext, managing global state becomes easier, further streamlining development and making your React applications more maintainable and efficient.
-
-## Before Hooks
-
-Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that seperation of concerns.
-
-```jsx
-import React, { Component } from 'react';
-
-class Profile extends Component {
- constructor(props) {
- super(props);
- this.state = {
- loading: false,
- user: {}
- };
- }
-
- componentDidMount() {
- this.updateProfile(this.props.id);
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.id !== this.props.id) {
- this.updateProfile(this.props.id);
- }
- }
-
- componentWillUnmount() {
- // do some unmounting actions
- }
-
- fetchUser(id) {
- // fetch users logic here
- }
-
- async updateProfile(id) {
- this.setState({ loading: true });
- // fetch users data
- await this.fetchUser(id);
- this.setState({ loading: false });
- }
- render() {
- // ... some jsx
- }
-}
-export default Profile;
-```
-
-## With Hooks
-
-With Hooks, functional components can handle both the presentational aspects and the business logic. This means you no longer need to separate your components strictly into presentational and container types.
-
-```jsx
-import React from 'react';
-
-const Profile = ({ id }) => {
- const [isLoading, setIsLoading] = useState(false);
- const [user, setUser] = useState({});
-
- useEffect(() => {
- updateProfile(id);
- }, [id]);
-
- const fetchUser = (id) => {
- // fetch users logic here
- };
-
- const updateProfile = async (id) => {
- setIsLoading(true);
- // fetch users data
- await fetchUser(id);
- setIsLoading(false);
- };
-
- return {
- // Some jsx code
- };
-};
-```
-
-## Exercise
-
-In this lesson we are going to refactor the Field component that we made into a react hook instead of following the render props pattern.
-
-Head over to the exercise and let's get started.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/06-Controlled/exercise.tsx b/src/course/02- lessons/06-Controlled/exercise.tsx
deleted file mode 100644
index 5d723eb..0000000
--- a/src/course/02- lessons/06-Controlled/exercise.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-import classNames from 'classnames';
-// ๐ฃ You can get rid of this eslint error comment when finished.
-// @ts-ignore
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { useEffect, useRef, useState } from 'react';
-import FocusLock from 'react-focus-lock';
-import { Button } from '../../../shared/components/Button/Button.component';
-
-interface IModal {
- isVisible: boolean;
- onClose: () => void;
- id: string;
- title: string;
- children: React.ReactNode | React.ReactNode[];
-}
-
-// For the full guide to making an accessible modal you can follow below to get every instance
-// โฟ๏ธ WCAG Modal Resource: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/
-// ๐ฃ You can get rid of this eslint error comment when finished.
-// @ts-ignore
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Modal = ({
- isVisible,
- // ๐ฃ You can get rid of this eslint error comment when finished.
- // @ts-ignore
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- onClose,
- id,
- title,
- children
-}: IModal) => {
- // 2a ๐จ๐ปโ๐ป Create a useRef and bind the ref to the div on line 58
-
- useEffect(() => {
- // โ๐ป When a modal is visible you want to navigate the focus from
- // the actioner (what caused the modal to open) to the content
- // โฟ๏ธ It helps the screenreader not get lost on the page
- // 2b - ๐จ๐ปโ๐ป Check if isVisible is true and the modal.current is defined before setting focus to the modal
- }, [isVisible]);
-
- // ๐ฃ You can get rid of this eslint error comment when finished.
- // @ts-ignore
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const onModalPress = (event: React.MouseEvent) => {
- // You may have noticed how we have added an onClose event to the container
- // and that is because customers normally click out of the modal to leave,
- // but if they click within the modal happens. The event "bubbles" up to
- // the container div and it closes the modal. Which is janky behaviour
- // ๐งช When you finish, remove the onModal press from the modal and try to click inside the modal then add it back
- // โ๐ป Stop propagation prevents an event from bubbling to the top.
- // โ๐ป https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation
- event.stopPropagation();
- event.preventDefault();
- };
-
- return (
-
-
- {/* โ๐ป SUPER important for meeting the WCAG quidelines is that you need focus, but locked within this div */}
- {/* When focus is landed in this box with a keyboard you can no longer get out so make sure you have a close button */}
- {/* โฟ๏ธ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! ๐ฆธ๐ปโโ๏ธ */}
-
-
- {/* 2f - ๐จ๐ปโ๐ปโฟ๏ธ Add id={`modal_title_${id}`} - this creates the relationship between the title and modal */}
-
{title}
- {/* 2g - ๐จ๐ปโ๐ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */}
-
-
- {/* 2h - ๐จ๐ปโ๐ปโฟ๏ธ Add id={`modal_body_${id}`} - this creates the relationship between the title and modal */}
-
{children}
-
-
-
- );
-};
-
-export const Exercise = () => {
- // 1a ๐จ๐ปโ๐ป Create a state hook variable with isVisible and setIsVisible
-
- // 1b ๐จ๐ปโ๐ป Create an onClose event that sets isVisible to false
-
- // 1c ๐จ๐ปโ๐ป Create an onOpen event that sets isVisible to true
-
- return (
- <>
- {/* 1d ๐จ๐ปโ๐ป Add the onClick={onOpen} event to the button
- โ๐ป This is an example of a Controlled component but in a HTML context.
- As a developer, we are providing the button with those props for the button
- to behave how we want it to behave, otherwise, it does nothing. */}
-
- {/* 1e ๐จ๐ปโ๐ป Check if isVisible (๐ Conditional Render Pattern) to render the Modal */}
- {/* Map the isVisible and onClose props to the Modal. The other props can be whatever you want */}
- >
- );
-};
diff --git a/src/course/02- lessons/06-Controlled/lesson.mdx b/src/course/02- lessons/06-Controlled/lesson.mdx
deleted file mode 100644
index 1b2b099..0000000
--- a/src/course/02- lessons/06-Controlled/lesson.mdx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Controlled Components Pattern
-
-The concept of controlled components involves creating components with highly predictable behavior by managing their state through props. A controlled components behavior changes based on the state passed to it as a prop.
-
-In the example below, the componentโs visibility is controlled by a prop, and it also accepts an onClose prop. In the parent component, invoking the onClose prop will hide the component. Clicking the "open" button in the parent component will then restore the components visibility.
-
-```jsx
-const Component = ({ isVisible, onClose }) => {
- return (
-
-
-
- );
-};
-
-const Page = () => {
- const [isVisible, setIsVisible] = useState(false);
-
- const onClose = () => {
- setIsVisible(false);
- };
-
- const onOpen = () => {
- setIsVisible(true);
- };
-
- return (
- <>
-
-
- >
- );
-};
-```
-
-This is good for components like a modal as they require state changes to happen outside of themselves before they appear in the browser. For example: I click a button like in the example above, it updates some state and then it renders the modal.
-
-## Exercise
-
-In this exercise we are going to build a Modal component which will be controlled via props.
-
-The task: The client has asked you to add a button on the page which will display more information in a popup once it is clicked.
-
-Head over to the exercise and let's get started.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/07-HigherOrderComponents/exercise.tsx b/src/course/02- lessons/07-HigherOrderComponents/exercise.tsx
deleted file mode 100644
index 40df327..0000000
--- a/src/course/02- lessons/07-HigherOrderComponents/exercise.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// ๐จ๐ปโ๐ป 2A Import IPokemon, IPokemonManagerActions, IPokemonManageState, withPokemon from './store';
-
-/**
- * Exercise: Implement a Higher Order Component
- *
- * What will we be doing?
- * We will be creating a Higher order component which will pass pokemon data
- * Using the same pattern that Redux used to do with their connect function
- *
- * Navigate your way to withPokemon to start.
- */
-
-// ๐จ๐ปโ๐ป 2B - Interfaces
-// ๐จ๐ปโ๐ป 2B.a Setup an interface called IMapStateToPropsComponentOneResponse.
-// This will just have pokemons: IPokemon[]; in the interface.
-
-// ๐จ๐ปโ๐ป 2B.b Setup an interface called IActionsComponentOneResponse.
-// This will just have fetchPokemons: (total: number) => Promise; in the interface.
-
-// ๐จ๐ปโ๐ป 2B.c Setup an interface called IComponentOneProps which extends IMapStateToPropsComponentOneResponse & IActionsComponentOneResponse.
-// This will just have title: string; in the interface.
-
-// ๐จ๐ปโ๐ป 2C - Setting up the mapStateToProps & mapActionsToProps
-// ๐จ๐ปโ๐ป 2C.a Setup a function called mapStateToProps which has a parameter state: IPokemonManagerState
-// it should return the IMapStateToPropsComponentOneResponse interface
-
-// ๐จ๐ปโ๐ป 2C.b Setup a function called mapActionsToProps which has a parameter actions: IPokemonManagerActions
-// it should return the IActionsComponentOneResponse interface
-
-// ๐จ๐ปโ๐ป 2D - Creating the component
-// ๐จ๐ปโ๐ป 2D.a Create a Component and it should have this params { pokemons, title, fetchPokemons }: IComponentProps
-// ๐จ๐ปโ๐ป 2D.b Make a useEffect with no dependencies and fetchPokemons - go wild with the total... why not.
-// ๐จ๐ปโ๐ป๐ 2D.c Return this markup
-{
- /*
-
{title}
-
- {pokemons.map((pokemon) => (
-
-
-
- ))}
-
- */
-}
-
-// ๐จ๐ปโ๐ป 2E - Applying the HOC
-// I want you to call:
-// const Exercise = withPokemons<
-// IMapStateToPropsComponentOneResponse, // We are defining a generic here
-// IActionsComponentOneResponse // We are defining a generic here
-// >(
-// mapStateToProps,
-// mapActionsToProps
-// )(Component);
-
-export const Exercise = () => {
- return null;
-};
diff --git a/src/course/02- lessons/07-HigherOrderComponents/lesson.mdx b/src/course/02- lessons/07-HigherOrderComponents/lesson.mdx
deleted file mode 100644
index aa8e0ba..0000000
--- a/src/course/02- lessons/07-HigherOrderComponents/lesson.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Higher Order Components Pattern
-
-In some cases you may want to have some logic that is consistent across your application. You could use hooks but this requires implementing some logic of code within each component you apply the hook which isn't sustainable as a higher order component.
-
-## What does it look like syntactically
-
-If you are familiar with functional programming this looks very similar to the "currying" pattern.
-
-```jsx
-// HigherOrderComponent.ts
-export const withHigherOrderComponent = (Component) => (props) => {
- const { isAuth } = useAuthentication();
-
- if (!isAuth) {
- return
;
-
-export default withHigherOrderComponent(Component);
-
-// App.tsx
-
-import Component from 'Component.tsx';
-
-const App = () => {
- return ;
-};
-```
-
-> In React it is good practice to use the **with** at the start as the name associates to a higher order component.
-
-Now let's breakdown what is actually going on here. You have the **withHigherOrderComponent** and it takes a Component as a prop and then you return another function with props which will normally be the props that the component will need that do not get provided by the higher order component.
-
-## When would this be useful?
-
-The logic in the higher order component is consolidated into one area which makes things more consistent across your application vs writing that line over and over again in every use case you need.
-
-## Exercise
-
-In this exercise we are going to go a bit wild and implement a very high level version of the Redux connect. The higher order component will pull information from a class which stores our data and then we will relay that data into the component.
-
-> Blast from the past this if you have done this with old Redux... sorry ๐
-
-## Feedback
-
-Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/07-HigherOrderComponents/withPokemon.tsx b/src/course/02- lessons/07-HigherOrderComponents/withPokemon.tsx
deleted file mode 100644
index b33eaa1..0000000
--- a/src/course/02- lessons/07-HigherOrderComponents/withPokemon.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-// ๐จ๐ปโ๐ป 1A import IPokemonManagerState, PokemonManager from './PokemonManager
-
-// ๐จ๐ปโ๐ป 1B Setup an interface called IPokemonManagerActions
-// It will contain just fetchPokemons: (total: number) => Promise; in the interface
-
-// ๐จ๐ปโ๐ป 1C Creating the HOC
-// ๐จ๐ปโ๐ป 1C.a export const withPokemons.
-// โ๐ป In typescript you can use something called generics which helps you set specific types
-// For specific scenarios. In our scenario, we want to have two Generic types where we specify
-// What data we need from state and what actions we need from state. This will make more sense
-// When we set it up in the exercise file. Example:
-// export const Func = (
-// prop: T
-// ) => {}
-// Func();
-// ๐จ๐ปโ๐ป 1C.b We need our props to look like this
-// export const withPokemons = (
-// mapper: (state: IPokemonManagerState) => TMapperResponse,
-// actions: (actions: IPokemonManagerActions) => TActionResponse
-// ) => {}
-
-// ๐จ๐ปโ๐ป1D - Returning the return
-// So the whole point of the HOC is to pass a Component into the first or second called For example:
-// withHOC(Component) or withHOC(funcA)(Component);
-// ๐จ๐ปโ๐ป1D.a We need to Return a function which looks like this:
-// (Component: React.FC) => {}
-// ๐จ๐ปโ๐ป1D.b And then we want to return another function
-// (props: any) - we could type this better but typescript isn't the core of this lesson.
-
-// ๐จ๐ปโ๐ป1E - Managing state from a class
-// We need to:
-// ๐จ๐ปโ๐ป1E.a - Create a useState with the initial value of new PokemonManager().getState()
-// ๐จ๐ปโ๐ป1E.b - Create a variable with useMemo(() => new PokemonManager(), []);
-// โ๐ป This is so we can prevent it from calling itself everytime the component re-rendered.
-// ๐จ๐ปโ๐ป1E.c - return the component with the following:
-// return (
-// {
-// await pokemonManager.fetchPokemons(total);
-// setPokemonManagerState(pokemonManager.getState());
-// }
-// })}
-// />
-// );
-
-// ๐ Finished with this file.
-// Take a step back and reflect what you have just done because this was more than just a HOC.
-// What you did was:
-/**
- * ๐ You made a complicated higher order component which did a double return based on the props it took.
- * ๐ We pretty much wrote the high level thinking of the old way of doing redux connect
- * ๐ You delve into TypeScript Generics and made this HOC more flexible.
- * ๐ You used static logic within an ES6 class and used react purely for state management.
- */
-
-// ๐จ๐ปโ๐ป Let's get this wired up into the presentational layer. Head over to exercise.tsx.
diff --git a/src/course/02- lessons/10-Compound/components/Accordion.tsx b/src/course/02- lessons/10-Compound/components/Accordion.tsx
deleted file mode 100644
index 3fe5c27..0000000
--- a/src/course/02- lessons/10-Compound/components/Accordion.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import classNames from 'classnames';
-import styles from './Accoridon.module.css';
-import { ChevronDown } from './ChevronDown';
-
-interface IAccordion {
- id: string;
- children:
- | React.ReactElement
- | React.ReactElement[];
- title: string;
-}
-
-interface IAccordionItem {
- id: string;
- children: React.ReactNode | React.ReactNode[];
- title: string;
- isSelected?: boolean;
- onClick?: VoidFunction;
- onFocus?: VoidFunction;
-}
-
-export const Accordion = ({ id, children, title }: IAccordion) => {
- // ๐จ๐ปโ๐ป 1B - Paste that useState here
-
- // ๐จ๐ปโ๐ป 1C - Replacing {children}
- // We need to map the children and apply the props to the AccordionItem here so we can manage the state within the accordion. It looks like this the syntax:
- // Children.map(children, (child: React.ReactElement, index) => cloneElement(child, { PROPS (look at the current props) }))
-
- // ๐จ๐ปโ๐ป 1D - Notice how there was an accordion-one in the id of the props on AccordionItem in exercise.tsx?
- // We need to use the index from the children map function as an identifier.
- /*
- isSelected: selectedAccordion === index,
- id: `${id}_${child.props.id}_${index}`,
- onClick: () => setSelectedAccordion(index),
- onFocus: () => setSelectedAccordion(index)
- */
-
- // Once this is completed return to the exercise.tsx file.
-
- // ๐จ๐ปโ๐ป 3B Now where you have the onClick which just does this - onClick: () => setSelectedAccordion(index) atm
- // Make it do this instead
- /*
- onClick: () => {
- if (child.props.onClick) {
- child.props.onClick();
- }
-
- setSelectedAccordion(index)
- }
- */
- // What is happening here now is that we are checking if the AccordionItem already has a onClick prop and firing that if it does exist as well as managing the local state of the accordion.
- return (
-
-
-);
diff --git a/src/course/02- lessons/10-Compound/components/Accoridon.module.css b/src/course/02- lessons/10-Compound/components/Accoridon.module.css
deleted file mode 100644
index 53889b7..0000000
--- a/src/course/02- lessons/10-Compound/components/Accoridon.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.accordionPanel {
- @apply py-0;
- @apply px-4;
- @apply h-0;
- @apply transition-all;
- @apply duration-300;
-}
-
-.accordionItem:focus-within .accordionPanel,
-.accordionPanelSelected {
- @apply p-4;
- @apply h-auto;
- @apply max-h-[1000px];
- @apply transition-all;
- @apply duration-300;
-}
-
-.accordionButton {
- @apply bg-blue-100;
- @apply hover:bg-blue-200;
- @apply text-blue-950;
-}
-
-.accordionItem:focus-within .accordionButton,
-.accordionButtonSelected {
- @apply bg-blue-950;
- @apply hover:bg-blue-950;
- @apply text-white;
-}
-
-.accordionIcon {
- @apply rotate-[-90deg];
-}
-
-.accordionItem:focus-within .accordionIcon,
-.accordionButtonSelected .accordionIcon {
- @apply rotate-0;
-}
diff --git a/src/course/02- lessons/10-Compound/components/ChevronDown.tsx b/src/course/02- lessons/10-Compound/components/ChevronDown.tsx
deleted file mode 100644
index a2303f4..0000000
--- a/src/course/02- lessons/10-Compound/components/ChevronDown.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-export const ChevronDown = () => (
-
-);
diff --git a/src/course/02- lessons/10-Compound/exercise.tsx b/src/course/02- lessons/10-Compound/exercise.tsx
deleted file mode 100644
index dfd7355..0000000
--- a/src/course/02- lessons/10-Compound/exercise.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { useState } from 'react';
-import { Accordion, AccordionItem } from './components/Accordion';
-
-/**
- * Exercise: Convert the current accordion implementation to use the compound pattern
- *
- * ๐ค Observations of this file
- * As you can see in this component we have some useState which is managing which accordion item is open at any given time. We need to move this logic into the Accordion component and pass down the props into the AccordionItem that way instead of managing it here in this file.
- *
- */
-
-// ๐จ๐ปโ๐ป 1A Copy the useState on line 14 and go to ./components/Accordion.tsx
-export const Exercise = () => {
- // ๐ฃ 2A Remove the useState and the isSelected, id, onClick, onFocus props from all the AccordionItems
-
- const [selectedAccordion, setSelectedAccordion] =
- useState();
-
- // ๐ค 3A (Bonus round) - now the customer wants to add event tracking when you click ONLY the first accordion item. Since the props now live in the accordion for onClick, we need to persist that onClick to the accordionItem if we specify one at this level. Add an onClick on the first AccordionItem with a console.log('TRACK') and then move over to the Accordion.tsx.
- return (
-
- setSelectedAccordion('accordion-one')}
- onFocus={() => setSelectedAccordion('accordion-one')}
- >
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at
- ante. Eros lacus faucibus consequat scelerisque proin
- volutpat. In pellentesque est curae; dapibus nisl risus
- sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
- Elit vel eros facilisi dis mauris magna posuere? Cum class
- viverra bibendum rutrum odio scelerisque scelerisque libero,
- nisl est convallis non. Ac convallis odio suspendisse velit
- mollis libero. Morbi enim blandit venenatis{' '}
- lorem!
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at
- ante. Eros lacus faucibus consequat scelerisque proin
- volutpat. In pellentesque est curae; dapibus nisl risus
- sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
- Elit vel eros facilisi dis mauris magna posuere? Cum class
- viverra bibendum rutrum odio scelerisque scelerisque libero,
- nisl est convallis non. Ac convallis odio suspendisse velit
- mollis libero. Morbi enim blandit venenatis{' '}
- lorem!
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at
- ante. Eros lacus faucibus consequat scelerisque proin
- volutpat. In pellentesque est curae; dapibus nisl risus
- sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
- Elit vel eros facilisi dis mauris magna posuere? Cum class
- viverra bibendum rutrum odio scelerisque scelerisque libero,
- nisl est convallis non. Ac convallis odio suspendisse velit
- mollis libero. Morbi enim blandit venenatis{' '}
- lorem!
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at
- ante. Eros lacus faucibus consequat scelerisque proin
- volutpat. In pellentesque est curae; dapibus nisl risus
- sociosqu penatibus. Lobortis pulvinar scelerisque lacus.
- Elit vel eros facilisi dis mauris magna posuere? Cum class
- viverra bibendum rutrum odio scelerisque scelerisque libero,
- nisl est convallis non. Ac convallis odio suspendisse velit
- mollis libero. Morbi enim blandit venenatis{' '}
- lorem!
-
-
-
- );
-};
diff --git a/src/course/02- lessons/10-Compound/lesson.mdx b/src/course/02- lessons/10-Compound/lesson.mdx
deleted file mode 100644
index 66b3236..0000000
--- a/src/course/02- lessons/10-Compound/lesson.mdx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Compound Components Pattern
-
-Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic โ working together.
-
-The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI. A good example is the select html element:
-
-```html
-
-```
-
-In the code above, the select element manages and shares its state implicitly with the options elements. Consequently, although there is no explicit state declaration, the select element knows what option the user selects.
-
-The compound component pattern is useful in building complex React components such as a switch, tab switcher, accordion, dropdowns, tag list, and more. It can be implemented either by using the Context API or the React.cloneElement function.
-
-## Exercise
-
-A requirement has come in to reuse the accordion in another location of our application. The current implementation of the accordion has it's state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/11-Slots/exercise.stories.tsx b/src/course/02- lessons/11-Slots/exercise.stories.tsx
deleted file mode 100644
index c3521e5..0000000
--- a/src/course/02- lessons/11-Slots/exercise.stories.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { Exercise } from './exercise';
-
-const meta: Meta = {
- title: 'Lessons/11 - Slots/02-Exercise',
- component: Exercise
-};
-
-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/11-Slots/exercise.tsx b/src/course/02- lessons/11-Slots/exercise.tsx
deleted file mode 100644
index 01dd4d3..0000000
--- a/src/course/02- lessons/11-Slots/exercise.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import classNames from 'classnames';
-import { HTMLAttributes } from 'react';
-
-// ๐จ๐ปโ๐ป 1A - Add two new types of "iconLeft" & "iconRight"
-interface IButton extends HTMLAttributes {
- className?: string;
- children: React.ReactNode | React.ReactNode[];
-}
-
-const buttonClasses =
- 'middle none center rounded-lg bg-blue-500 py-3 px-6 font-sans text-xs font-bold uppercase text-white shadow-md shadow-blue-500/20 transition-all hover:shadow-lg hover:shadow-blue-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none inline-flex items-center justify-center';
-
-// ๐จ๐ปโ๐ป 1B - Extract those types out from the props and then add iconLeft above children and iconRight below
-// ๐ 1C - styling - icon && {icon}
-export const Button = ({ className, children, ...rest }: IButton) => {
- return (
-
- );
-};
-
-// ๐จ๐ปโ๐ป 1D - Add iconLeft={IconOne} from the icons folder to the first button
-// ๐จ๐ปโ๐ป 1E - Add iconRight={IconTwo} from the icons folder to the second button
-// ๐จ๐ปโ๐ป 1F - Add iconRight and iconLeft to the third button.
-// Check storybook, you should see some black icons.... Why?
-// ๐ 2A - head over to ./icons/index.tsx
-export const Exercise = () => (
-
-
-
-
-
-);
diff --git a/src/course/02- lessons/11-Slots/lesson.mdx b/src/course/02- lessons/11-Slots/lesson.mdx
deleted file mode 100644
index 2093db8..0000000
--- a/src/course/02- lessons/11-Slots/lesson.mdx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Slots Pattern
-
-The slots pattern is very similar to the [render props pattern](?path=/docs/lessons-03-render-props-pattern-01-lesson--docs) but it has a slight difference which justifies why you would do one over the other.
-
-Here is an example of how you do a render prop pattern:
-
-```jsx
-const HelloWorld = ({ render }) => render({ hello: 'world' });
-
-// How you would use it.
-
{hello}
} />;
-```
-
-The key thing here is that we are passing some state from our react component which can be used with components outside. What if we didn't need the the hello prop? We could still use this pattern but an alternative would be a slot.
-
-## Children is a slot & a render prop
-
-Now the easiest way to form a connection with the wording of "slot" is to think of children. The children prop in React can either be a slot of a render prop in react.
-
-```
-{children} // slot
-{children({ hello: 'world' })} // render prop
-```
-
-## Using slots as props
-
-Here is an example of us using the HelloWorld example above.
-
-```jsx
-const HelloWorld = ({ slot }) =>
Hello {slot}
;
-
-// How you would use it.
-World} />;
-```
-
-The pros to using this approach are:
-
-- Useful for layout components where you make the responsibility of the layout purely column based and port the header/footer/nav using slots.
-- Removing many if statements and prop drilling from your presentational component
-
-The cons of using this approach:
-
-- If you need to use the same UI in multiple places you will duplicate code (can be solved by making an abstraction component)
-
-## Exercise
-
-So in the exercise we have a task to extend the functionality of our Button component. The design team want to be able to put an icon on the left side of the button and/or on the right side. Our task is to implement this feature in a way so that any icon can be put inside this component.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/12-Portals/components/modal.tsx b/src/course/02- lessons/12-Portals/components/modal.tsx
deleted file mode 100644
index 749bbd0..0000000
--- a/src/course/02- lessons/12-Portals/components/modal.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import classNames from 'classnames';
-import { useEffect, useRef } from 'react';
-// ๐จ๐ปโ๐ป 1B - import { createPortal } from 'react-dom';
-import FocusLock from 'react-focus-lock';
-import { Button } from '../../../../shared/components/Button/Button.component';
-
-interface IModal {
- isVisible: boolean;
- onClose: () => void;
- id: string;
- title: string;
- children: React.ReactNode | React.ReactNode[];
-}
-
-export const Modal = ({
- isVisible,
- onClose,
- id,
- title,
- children
-}: IModal) => {
- const modal = useRef(null);
-
- useEffect(() => {
- if (isVisible && modal.current) {
- modal.current.focus();
- }
- }, [isVisible]);
-
- const onModalPress = (event: React.MouseEvent) => {
- event.stopPropagation();
- event.preventDefault();
- };
-
- // ๐จ๐ปโ๐ป 1C - call createPortal(modalCode, document.body);
- // ๐งช Test the storybook and look at how you can all of a sudden click the pay now button
- // This isn't saying the solution to override z-index is to use portal but more of the sense that if you need something
- // put at the root of the DOM but do not wish to implement something extremely complex or app level then portal is handy for this.
- return (
-
-
-
-
-
{title}
-
-
-
{children}
-
-
-
- );
-};
diff --git a/src/course/02- lessons/12-Portals/exercise.stories.tsx b/src/course/02- lessons/12-Portals/exercise.stories.tsx
deleted file mode 100644
index 63d4c26..0000000
--- a/src/course/02- lessons/12-Portals/exercise.stories.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { Exercise } from './exercise';
-
-const meta: Meta = {
- title: 'Lessons/12 - Portals/02-Exercise',
- component: Exercise
-};
-
-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/12-Portals/exercise.tsx b/src/course/02- lessons/12-Portals/exercise.tsx
deleted file mode 100644
index 7158c39..0000000
--- a/src/course/02- lessons/12-Portals/exercise.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useState } from 'react';
-import { Modal } from './components/modal';
-import { Button } from '../../../shared/components/Button/Button.component';
-
-// ๐จ๐ปโ๐ป 1A - have a look at the current implementation of the modal and then go to components/modal.tsx
-
-export const Exercise = () => {
- const [isVisible, setIsVisible] = useState(false);
- const [isComplete, setIsComplete] = useState(false);
-
- const onClose = () => {
- setIsVisible(false);
- };
-
- const onOpen = () => {
- setIsVisible(true);
- };
-
- const onCheckout = () => {
- setIsComplete(true);
- };
-
- return (
- // ๐งช We have z-index 10 on the section and then z-9998 on a div that's purposely there. Our Modal has a z-20 which means:
- // section z-10
- // modal z-20 (but this means z-20 within the z-10) think of it as a sub layer.
- // the bug is 9998 and a css hack for the pay now is 9999
-
-
- {isComplete && (
- <>
-
- Payment Successful
-
-
Well done you did it!
- >
- )}
-
- {!isComplete && (
- <>
-
Payment Page
-
-
- Please see your selected options from the previous steps
- before continuing.
-
-
-
-
- Delivery Details
-
-
-
12 john doe street, Manchester, M12 3RT
-
-
-
-
-
- Make Payment
-
-
-
- >
- )}
- {isVisible && !isComplete && (
-
-
-
- )}
-
- );
-};
diff --git a/src/course/02- lessons/12-Portals/lesson.mdx b/src/course/02- lessons/12-Portals/lesson.mdx
deleted file mode 100644
index 8b7a9f4..0000000
--- a/src/course/02- lessons/12-Portals/lesson.mdx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Meta } from '@storybook/blocks';
-
-
-
-# Portals Pattern
-
-A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is how it looks in React:
-
-```jsx
-import { createPortal } from 'react-dom';
-
-// ...
-
-
-
This child is placed in the parent div.
- {createPortal(
-
This child is placed in the document body.
,
- document.body
- )}
-
;
-```
-
-Which in html, will translate to:
-
-```html
-
-
- My react app
-
-
-
-
-
This child is placed in the parent div.
-
-
-
-
This child is placed in the document body.
-
-
-```
-
-Portal benefits:
-
-- Simplified state management
-- No clashes with z-index as the modal is at the root of the DOM.
-
-## Exercise
-
-In the current application when a customer clicks the checkout cta and the payment popup appears, the customer cannot click the pay now button in the modal.
-
-## Feedback
-
-Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
-
-[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..531c811
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.stories.tsx
@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { userEvent, within, expect } from '@storybook/test';
+
+import { PokemonTrainerStatus } from './exercise';
+
+const meta: Meta = {
+ title:
+ 'Lessons/๐ฅ Bronze/๐ Conditional Rendering Pattern/02-Exercise',
+ component: PokemonTrainerStatus
+};
+
+export default meta;
+type Story = StoryObj;
+
+const trainerName = 'Ash';
+
+/*
+ * 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 ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await userEvent.click(
+ canvas.getByRole('button', { name: '๐ฏ Challenge Gym Leader' })
+ );
+
+ await expect(
+ canvas.getByText(`Welcome Gym Leader ${trainerName}! ๐`)
+ ).toBeInTheDocument();
+ await expect(
+ canvas.queryByRole('button', { name: '๐ฏ Challenge Gym Leader' })
+ ).toBeNull();
+
+ await userEvent.click(
+ canvas.getByRole('button', { name: '๐ Reset Journey' })
+ );
+
+ await expect(
+ canvas.queryByText(`Welcome Gym Leader ${trainerName}! ๐`)
+ ).toBeNull();
+ await expect(
+ canvas.queryByRole('button', { name: '๐ Reset Journey' })
+ ).toBeNull();
+ },
+ args: {
+ trainerName
+ }
+};
\ No newline at end of file
diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx
new file mode 100644
index 0000000..d1abf80
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/exercise/exercise.tsx
@@ -0,0 +1,23 @@
+import { Button } from '@shared/components/Button/Button.component';
+
+interface ITrainerProps {
+ trainerName: string;
+}
+
+// 1g - ๐ฃ The ignore lint rules can be removed now
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-expect-error
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const PokemonTrainerStatus = (props: ITrainerProps) => {
+ // 1a - ๐ป add a useState that has false as default. Name the variable [hasGymBadges, setHasGymBadges]
+
+ // 1b - ๐ป create me a onEarnBadge function which setHasGymBadges to be true
+
+ // 1c - ๐ป create me a onLoseBadges function which setHasGymBadges to be false
+
+ // 1d - ๐ป if hasGymBadges, return a button called "Reset Journey" with the onClick of onLoseBadges
+ // 1e - ๐ป if hasGymBadges, return some text called "Welcome Gym Leader {props.trainerName}! ๐"
+
+ // 1f - ๐ป add onClick function onEarnBadge to the button
+ return ;
+};
diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx
new file mode 100644
index 0000000..74bb2aa
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { userEvent, within, expect } from '@storybook/test';
+
+import { PokemonTrainerStatus } from './final';
+
+const meta: Meta = {
+ title:
+ 'Lessons/๐ฅ Bronze/๐ Conditional Rendering Pattern/03-Final',
+ component: PokemonTrainerStatus
+};
+
+export default meta;
+type Story = StoryObj;
+
+const trainerName = 'Ash';
+
+/*
+ * 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 ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await userEvent.click(
+ canvas.getByRole('button', { name: '๐ฏ Challenge Gym Leader' })
+ );
+
+ await expect(
+ canvas.getByText(`Welcome Gym Leader ${trainerName}! ๐`)
+ ).toBeInTheDocument();
+
+ await userEvent.click(
+ canvas.getByRole('button', { name: '๐ Reset Journey' })
+ );
+
+ await expect(
+ canvas.getByRole('button', { name: '๐ฏ Challenge Gym Leader' })
+ ).toBeInTheDocument();
+ },
+ args: {
+ trainerName
+ }
+};
\ No newline at end of file
diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx
new file mode 100644
index 0000000..a7c89a8
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/final/final.tsx
@@ -0,0 +1,39 @@
+import { useState } from 'react';
+import { Button } from '@shared/components/Button/Button.component';
+
+interface ITrainerProps {
+ trainerName: string;
+}
+
+export const PokemonTrainerStatus = (props: ITrainerProps) => {
+ const [hasGymBadges, setHasGymBadges] = useState(false);
+
+ const onEarnBadge = () => {
+ setHasGymBadges(true);
+ };
+
+ const onLoseBadges = () => {
+ setHasGymBadges(false);
+ };
+
+ return (
+
+ {!hasGymBadges && (
+
+
๐ Pokemon Trainer
+
Ready to challenge the Gym Leader?
+
+
+ )}
+ {hasGymBadges && (
+
+
+ Welcome Gym Leader {props.trainerName}! ๐
+
+
๐ฅ You've earned your gym badges!
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx
new file mode 100644
index 0000000..0b239a5
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/ConditionalRendering/lesson.mdx
@@ -0,0 +1,108 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ Conditional Rendering Pattern
+
+The conditional rendering pattern is a way to dynamically change UI based on what values are set at that time. The most common way that this is done is by using an if statement. For example:
+
+```jsx
+const TrainerStatus = (props) => {
+ if (props.hasGymBadges) {
+ return
Welcome Gym Leader {props.trainerName}!
;
+ } else {
+ return
Welcome Pokemon Trainer!
;
+ }
+};
+```
+
+There are many syntactical ways you can do the same as above such as the ternary:
+
+```jsx
+const TrainerStatus = (props) => {
+ return props.hasGymBadges ? (
+
Welcome Gym Leader {props.trainerName}!
+ ) : (
+
Welcome Pokemon Trainer!
+ );
+};
+```
+
+Or you can use the AND syntax:
+
+```jsx
+const TrainerStatus = (props) => {
+ return props.hasGymBadges &&
Welcome Gym Leader {props.trainerName}!
;
+};
+```
+
+If I had a lot of complexity in this component that still would be getting executed but the component will just return nothing if the trainer doesn't have badges. The best way to return something like this is to do the conditional render outside of the component, for example:
+
+```jsx
+const GymLeaderWelcome = (props) => {
+ return
Welcome Gym Leader {props.trainerName}
;
+};
+
+const TrainerDashboard = (props) => {
+ return (
+
+ {/* Other components */}
+ {props.hasGymBadges && }
+
+ );
+};
+```
+
+### Event driven rendering
+
+There may be times when you need to conditionally render a component based on an event that has been changed. This example conditionally renders a wild Pokemon encounter when you click the explore button.
+
+```jsx
+const PokemonExplorer = () => {
+ const [wildPokemonFound, setWildPokemonFound] = useState(false);
+
+ const startExploring = () => {
+ setWildPokemonFound(true);
+ };
+
+ const stopExploring = () => {
+ setWildPokemonFound(false);
+ };
+
+ return (
+ <>
+
+ {wildPokemonFound && (
+
+
๐ฟ A wild Pikachu appeared!
+
+ )}
+ >
+ );
+};
+```
+
+## Exercise
+
+In the first exercise we are going to look into building a Pokemon trainer status system that shows different content based on whether the trainer has earned gym badges. Go to the exercise.tsx inside the ConditionalRendering folder and start the exercise. Once completed, the Tests will show as passed in the storybook "Interactions" addon section.
+
+## When to use this pattern?
+
+**Use conditional rendering for:**
+- **Dynamic UI**: Show/hide components based on state or props
+- **User Permissions**: Display content based on user roles
+- **Loading States**: Show spinners while data loads
+- **Error Handling**: Display error messages when needed
+
+**Avoid when:**
+- **Complex Logic**: Use separate components for complex conditions
+- **Performance**: Avoid heavy computations in render conditions
+- **Readability**: Don't nest too many ternary operators
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/05-Hooks/components.tsx b/src/course/02-lessons/01-Bronze/Hooks/components.tsx
similarity index 64%
rename from src/course/02- lessons/05-Hooks/components.tsx
rename to src/course/02-lessons/01-Bronze/Hooks/components.tsx
index 2adee81..ae8a9d3 100644
--- a/src/course/02- lessons/05-Hooks/components.tsx
+++ b/src/course/02-lessons/01-Bronze/Hooks/components.tsx
@@ -1,9 +1,9 @@
-import { Input } from '../../../shared/components/Input/Input.component';
-import { Label } from '../../../shared/components/Label/Label.component';
-import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component';
+import { Input } from '@shared/components/Input/Input.component';
+import { Label } from '@shared/components/Label/Label.component';
+import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component';
import { HTMLAttributes } from 'react';
-export interface ITextFieldProps {
+export interface TextFieldProps {
hasError: boolean;
errorMessage?: string;
id: string;
@@ -19,7 +19,7 @@ export const TextFieldComponent = ({
id,
name,
label
-}: ITextFieldProps) => (
+}: TextFieldProps) => (
diff --git a/src/course/02- lessons/08-Provider/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx
similarity index 87%
rename from src/course/02- lessons/08-Provider/exercise.stories.tsx
rename to src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx
index 6feb89c..2781dbc 100644
--- a/src/course/02- lessons/08-Provider/exercise.stories.tsx
+++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/08 - Provider Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Bronze/๐ฃ Hooks Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx
new file mode 100644
index 0000000..65db7cc
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx
@@ -0,0 +1,191 @@
+import { useState } from 'react';
+import { Button } from '@shared/components/Button/Button.component';
+
+/*
+ * Observations
+ * ๐ The current implementation uses the Render Props Pattern
+ * We need to refactor this into a custom hook for Pokemon capture mechanics
+ */
+
+interface IPokemonCaptureProps {
+ area: string;
+ children: (captureState: {
+ wildPokemon: Pokemon | null;
+ pokeballs: number;
+ capturing: boolean;
+ capturedPokemon: Pokemon[];
+ attemptCapture: () => void;
+ encounterPokemon: () => void;
+ runAway: () => void;
+ restockPokeballs: (amount?: number) => void;
+ }) => React.ReactNode;
+}
+
+interface Pokemon {
+ id: number;
+ name: string;
+ type: string;
+ captureRate: number;
+}
+
+const WILD_POKEMON = [
+ { id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 },
+ { id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 },
+ { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 }
+];
+
+// 1A ๐ป - We need to refactor this to be called usePokemonCapture
+export const PokemonCaptureSystem = ({
+ children
+}: IPokemonCaptureProps) => {
+ const [wildPokemon, setWildPokemon] = useState(
+ null
+ );
+ const [pokeballs, setPokeballs] = useState(10);
+ const [capturing, setCapturing] = useState(false);
+ const [capturedPokemon, setCapturedPokemon] = useState(
+ []
+ );
+
+ const encounterPokemon = () => {
+ const randomPokemon =
+ WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)];
+ setWildPokemon(randomPokemon);
+ };
+
+ const attemptCapture = async () => {
+ if (!wildPokemon || pokeballs <= 0) return;
+
+ setCapturing(true);
+ setPokeballs((prev) => prev - 1);
+
+ // Simulate capture attempt
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+
+ const success = Math.random() < wildPokemon.captureRate;
+ if (success) {
+ setCapturedPokemon((prev) => [...prev, wildPokemon]);
+ setWildPokemon(null);
+ }
+
+ setCapturing(false);
+ };
+
+ const runAway = () => {
+ setWildPokemon(null);
+ };
+
+ const restockPokeballs = (amount: number = 5) => {
+ setPokeballs((prev) => prev + amount);
+ };
+
+ // 1C ๐ป - Just return the object instead of children
+ return children({
+ wildPokemon,
+ pokeballs,
+ capturing,
+ capturedPokemon,
+ attemptCapture,
+ encounterPokemon,
+ runAway,
+ restockPokeballs
+ });
+};
+
+// 2A ๐ค - What if we wanted to use this capture logic in multiple components?
+// Let's make a component which uses the usePokemonCapture hook and takes an area prop
+
+export const Exercise = () => {
+ // 1E ๐ป - call the usePokemonCapture hook here
+ return (
+
+
+ ๐ฟ Pokemon Capture System
+
+
+ {/* 1F ๐ฃ - Remove the PokemonCaptureSystem component and use the hook directly */}
+
+ {({
+ wildPokemon,
+ pokeballs,
+ capturing,
+ capturedPokemon,
+ attemptCapture,
+ encounterPokemon,
+ runAway,
+ restockPokeballs
+ }) => (
+
+ Using the usePokemonCapture() hook to manage capture mechanics
+ across different areas.
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx
new file mode 100644
index 0000000..dfd6e6b
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx
@@ -0,0 +1,119 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฃ Hooks Pattern
+
+React Hooks, introduced in React 16.8, revolutionize the traditional presentational and container component pattern by allowing functional components to manage state and side effects directly. This means you no longer need to separate components into presentational (stateless) and container (stateful) types. Hooks like useState and useEffect enable functional components to handle both UI rendering and business logic, simplifying code and improving readability.
+
+Additionally, Hooks enhance reusability and reduce prop drilling. Custom Hooks allow you to extract and share logic across components, promoting a more modular codebase. With features like useContext, managing global state becomes easier, further streamlining development and making your React applications more maintainable and efficient.
+
+## Before Hooks
+
+Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that separation of concerns.
+
+```jsx
+import React, { Component } from 'react';
+
+class PokemonCapture extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ capturing: false,
+ wildPokemon: null,
+ pokeballs: 10,
+ capturedPokemon: []
+ };
+ }
+
+ componentDidMount() {
+ this.encounterWildPokemon(this.props.area);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.area !== this.props.area) {
+ this.encounterWildPokemon(this.props.area);
+ }
+ }
+
+ componentWillUnmount() {
+ // cleanup capture animations
+ }
+
+ encounterWildPokemon(area) {
+ // find random pokemon in area
+ }
+
+ async attemptCapture(pokemon) {
+ this.setState({ capturing: true });
+ // capture logic with success rate
+ await this.throwPokeball(pokemon);
+ this.setState({ capturing: false });
+ }
+ render() {
+ // ... pokemon capture jsx
+ }
+}
+export default PokemonCapture;
+```
+
+## With Hooks
+
+With Hooks, functional components can handle both the presentational aspects and the business logic. This means you no longer need to separate your components strictly into presentational and container types.
+
+```jsx
+import React, { useState, useEffect } from 'react';
+
+const PokemonCapture = ({ area }) => {
+ const [capturing, setCapturing] = useState(false);
+ const [wildPokemon, setWildPokemon] = useState(null);
+ const [pokeballs, setPokeballs] = useState(10);
+ const [capturedPokemon, setCapturedPokemon] = useState([]);
+
+ useEffect(() => {
+ encounterWildPokemon(area);
+ }, [area]);
+
+ const encounterWildPokemon = (area) => {
+ // find random pokemon in area
+ };
+
+ const attemptCapture = async (pokemon) => {
+ setCapturing(true);
+ // capture logic with success rate
+ await throwPokeball(pokemon);
+ setCapturing(false);
+ };
+
+ return (
+ // Pokemon capture interface jsx
+ );
+};
+```
+
+## Exercise
+
+In this lesson we are going to refactor the Pokemon capture component into a custom React hook called **usePokemonCapture()** instead of following the render props pattern.
+
+You'll create a hook that manages capture attempts, pokeball inventory, success rates, and wild Pokemon encounters.
+
+Head over to the exercise and let's get started.
+
+## When to use this pattern?
+
+**Use hooks for:**
+- **State Management**: Replace class component state with useState
+- **Side Effects**: Handle API calls, subscriptions with useEffect
+- **Logic Reuse**: Extract common logic into custom hooks
+- **Modern React**: Preferred approach for new components
+
+**Avoid when:**
+- **Legacy Code**: Existing class components work fine
+- **Simple Components**: Pure presentational components don't need hooks
+- **Over-abstraction**: Don't create hooks for single-use logic
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx
similarity index 85%
rename from src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx
index d3892a6..3bd9880 100644
--- a/src/course/02- lessons/04-PresentationalAndContainer/exercise.stories.tsx
+++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.stories.tsx
@@ -4,7 +4,7 @@ import { BrandPageOne, BrandPageTwo } from './exercise';
const meta: Meta = {
title:
- 'Lessons/04 - Presentational & Container Pattern/02-Exercise',
+ 'Lessons/๐ฅ Bronze/๐ญ Presentational & Container Pattern/02-Exercise',
component: BrandPageOne
};
diff --git a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx
similarity index 92%
rename from src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx
index cb3d075..9b199da 100644
--- a/src/course/02- lessons/04-PresentationalAndContainer/exercise.tsx
+++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/exercise/exercise.tsx
@@ -1,12 +1,12 @@
import { useEffect, useState } from 'react';
-import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component';
+import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component';
import {
ICheckoutData,
useBrandOnePayment,
useCheckout
-} from './mocks';
-import { Skeleton } from '../../../shared/components/Skeleton/Skeleton.component';
-import { Button } from '../../../shared/components/Button/Button.component';
+} from '../mocks';
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import { Button } from '@shared/components/Button/Button.component';
interface IPaymentTemplate {
hasPaymentFailed: boolean;
@@ -22,6 +22,7 @@ interface IPaymentTemplate {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-pattern
const PaymentTemplate = ({}: IPaymentTemplate) => null;
// 1B ๐จ๐ปโ๐ป - Use the Payment template and pass down the props it needs
diff --git a/src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx
similarity index 84%
rename from src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx
index a6bd4a5..5b3caf5 100644
--- a/src/course/02-solutions/04-PresentationalAndContainer/final.stories.tsx
+++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.stories.tsx
@@ -3,7 +3,8 @@ import type { Meta, StoryObj } from '@storybook/react';
import { BrandPageOne, BrandPageTwo } from './final';
const meta: Meta = {
- title: 'Lessons/04 - Presentational & Container Pattern/03-Final',
+ title:
+ 'Lessons/๐ฅ Bronze/๐ญ Presentational & Container Pattern/03-Final',
component: BrandPageOne
};
diff --git a/src/course/02-solutions/04-PresentationalAndContainer/final.tsx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.tsx
similarity index 93%
rename from src/course/02-solutions/04-PresentationalAndContainer/final.tsx
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.tsx
index 028dc5e..e539017 100644
--- a/src/course/02-solutions/04-PresentationalAndContainer/final.tsx
+++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/final/final.tsx
@@ -1,13 +1,13 @@
import { useEffect, useState } from 'react';
-import { ErrorMessage } from '../../../shared/components/ErrorMessage/ErrorMessage.component';
+import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component';
import {
useBrandOnePayment,
useCheckout,
useBrandTwoPayment,
ICheckoutData
-} from './mocks';
-import { Button } from '../../../shared/components/Button/Button.component';
-import { Skeleton } from '../../../shared/components/Skeleton/Skeleton.component';
+} from '../mocks';
+import { Button } from '@shared/components/Button/Button.component';
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
interface IPaymentTemplate {
hasPaymentFailed: boolean;
diff --git a/src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx
similarity index 97%
rename from src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx
index b784a02..b01ec5a 100644
--- a/src/course/02- lessons/04-PresentationalAndContainer/lesson.mdx
+++ b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/lesson.mdx
@@ -1,8 +1,8 @@
import { Meta } from '@storybook/blocks';
-
+
-# Presentational & Container Pattern
+# ๐ญ Presentational & Container Pattern
This is an old pattern created by Dan Abramov. However, he doesn't suggest to use this pattern anymore as it was more linked to before reack hooks. This pattern has become more obsolete since hooks came into the mix.
diff --git a/src/course/02- lessons/04-PresentationalAndContainer/mocks.ts b/src/course/02-lessons/01-Bronze/PresentationalAndContainer/mocks.ts
similarity index 100%
rename from src/course/02- lessons/04-PresentationalAndContainer/mocks.ts
rename to src/course/02-lessons/01-Bronze/PresentationalAndContainer/mocks.ts
diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..83e2daf
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.stories.tsx
@@ -0,0 +1,37 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Bronze/๐งฉ Props Combination Pattern/02-Exercise',
+ component: Exercise
+};
+
+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 = {
+ args: {
+ pokemonName: 'Charizard',
+ pokemonType: 'Fire/Flying',
+ pokemonHp: 180,
+ pokemonLevel: 55,
+ attackName: 'Fire Blast',
+ attackDamage: 120,
+ attackDescription: 'A powerful fire attack that may leave the target with a burn.',
+ imageAltText: 'Charizard breathing fire',
+ imageUrlSmall: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png',
+ imageUrlMedium: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png',
+ imageUrlLarge: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png',
+ cardClassName: 'shadow-xl',
+ nameClassName: 'text-red-600',
+ typeClassName: 'bg-red-500',
+ hpClassName: 'text-red-700',
+ attackClassName: 'border-red-400',
+ imageClassName: 'hover:scale-105 transition-transform'
+ }
+};
diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx
new file mode 100644
index 0000000..e1dd376
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/PropsCombination/exercise/exercise.tsx
@@ -0,0 +1,131 @@
+import classnames from 'classnames';
+
+/*
+
+ 1a๐ป group the following together:
+
+ * pokemon - pokemonName, pokemonType, pokemonHp, pokemonLevel
+ * attack - attackName, attackDamage, attackDescription
+ * image - imageAltText, imageUrlSmall, imageUrlMedium, imageUrlLarge
+ * styling - cardClassName, nameClassName, typeClassName, hpClassName, attackClassName, imageClassName
+
+*/
+interface IPokemonCardProps {
+ pokemonName: string;
+ pokemonType: string;
+ pokemonHp: number;
+ pokemonLevel: number;
+ attackName: string;
+ attackDamage: number;
+ attackDescription: string;
+ imageAltText: string;
+ imageUrlSmall: string;
+ imageUrlMedium: string;
+ imageUrlLarge: string;
+ cardClassName?: string;
+ nameClassName?: string;
+ typeClassName?: string;
+ hpClassName?: string;
+ attackClassName?: string;
+ imageClassName?: string;
+}
+
+/*
+ 1b๐ป Update the props to match the new grouped types defined above.
+*/
+export const Exercise = ({
+ pokemonName,
+ pokemonType,
+ pokemonHp,
+ pokemonLevel,
+ attackName,
+ attackDamage,
+ attackDescription,
+ imageAltText,
+ imageUrlSmall,
+ imageUrlMedium,
+ imageUrlLarge,
+ cardClassName,
+ nameClassName,
+ typeClassName,
+ hpClassName,
+ attackClassName,
+ imageClassName
+}: IPokemonCardProps) => {
+ /*
+ 2a ๐ค Could we destructure the image to be [small, medium, large]?
+ */
+ /*
+ 1c๐ป Update the props in the jsx to use the grouped structure
+ */
+ return (
+
+
+
+ โญ Level {pokemonLevel}
+
+
+
+
+
+
+
+
+
+
+
+
+ {pokemonName}
+
+
+
+
+ {pokemonType} Type
+
+
+ โค๏ธ {pokemonHp} HP
+
+
+
+ {pokemon.type} Type
+
+
+ โค๏ธ {pokemon.hp} HP
+
+
+
+
+
+ โก {attack.name} - {attack.damage} damage
+
+
+ {attack.description}
+
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx
new file mode 100644
index 0000000..0978a63
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/PropsCombination/lesson.mdx
@@ -0,0 +1,76 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐งฉ Props Combination Pattern
+
+Props are used to pass data from one component to another. The prop combination pattern groups related props into a single object. This object is then passed as a single prop to a component.
+
+Some benefits of this pattern include reduction of boilerplate code, improving code readability and maintainability.
+
+Let's take a look at an example with a Pokemon trading card:
+
+```jsx
+const PokemonCard = ({
+ pokemonName,
+ pokemonType,
+ pokemonHp,
+ pokemonLevel,
+ attackName,
+ attackDamage,
+ attackDescription,
+ imageAltText,
+ imageUrlSmall,
+ imageUrlMedium,
+ imageUrlLarge,
+ cardClassName,
+ nameClassName,
+ typeClassName,
+ statsClassName,
+ attackClassName
+}) => {/* Lots of Pokemon card code here... */};
+```
+
+Yes... this is pretty wild but it's very common to see this in the real world. What we have going into this Pokemon card component is:
+
+- pokemon - name, type, hp, level
+- attack - name, damage, description
+- image - the image sources and alt text
+- styling - all className props
+
+Now look at this when it is grouped:
+
+```jsx
+const PokemonCard = ({ pokemon, attack, image, styling }) => (
+ {/* Lots of Pokemon card code here... */}
+);
+```
+
+It is now a lot simpler to understand, we know the component needs pokemon data, an attack, an image, and styling options. If we then wanted to, we could easily add more pokemon stats or attacks.
+
+## Exercise
+
+In this exercise we are going to do the same thing that we did to the Pokemon card snippet above and then we have some extras to add to it so it will get you thinking about how to change the props if necessary.
+
+You'll be refactoring a Pokemon trading card component that currently has too many individual props into a cleaner, grouped structure.
+
+Head over to the exercise file and let's begin.
+
+## When to use this pattern?
+
+**Use props combination for:**
+- **Related Data**: Group logically related props together
+- **Reducing Clutter**: When components have many individual props
+- **API Design**: Creating cleaner component interfaces
+- **Maintainability**: Easier to add/remove related properties
+
+**Avoid when:**
+- **Simple Components**: Few props don't need grouping
+- **Unrelated Props**: Don't force unrelated props together
+- **Performance**: Grouping can cause unnecessary re-renders
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/09-StateReducer/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx
similarity index 87%
rename from src/course/02- lessons/09-StateReducer/exercise.stories.tsx
rename to src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx
index 17ae5bb..0f6fdc4 100644
--- a/src/course/02- lessons/09-StateReducer/exercise.stories.tsx
+++ b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/09 - State Reducer Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Bronze/๐ฐ Slots Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx
new file mode 100644
index 0000000..2c0a163
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/Slots/exercise/exercise.tsx
@@ -0,0 +1,201 @@
+import classNames from 'classnames';
+
+// ๐ป 1A - This component uses individual props for each map location. Can we refactor it to use slots instead?
+interface IPokemonMap {
+ className?: string;
+
+ // North area props
+ showNorthArea?: boolean;
+ northAreaName?: string;
+ northAreaIcon?: string;
+ northAreaColor?: string;
+
+ // South area props
+ showSouthArea?: boolean;
+ southAreaName?: string;
+ southAreaIcon?: string;
+ southAreaColor?: string;
+
+ // East area props
+ showEastArea?: boolean;
+ eastAreaName?: string;
+ eastAreaIcon?: string;
+ eastAreaColor?: string;
+
+ // West area props
+ showWestArea?: boolean;
+ westAreaName?: string;
+ westAreaIcon?: string;
+ westAreaColor?: string;
+
+ // Center area props
+ showCenterArea?: boolean;
+ centerAreaName?: string;
+ centerAreaIcon?: string;
+ centerAreaColor?: string;
+}
+
+const mapContainerClasses =
+ 'grid grid-cols-3 grid-rows-3 gap-2 w-80 h-80 p-4 bg-green-100 rounded-lg border-2 border-green-300';
+const areaClasses =
+ 'flex flex-col items-center justify-center p-3 rounded-lg border-2 text-sm font-bold text-white shadow-md';
+
+// ๐ป 1B - Look at all these props and conditional logic! This is hard to maintain.
+// ๐ป 1C - Refactor this to use northSlot, southSlot, eastSlot, westSlot, centerSlot instead
+export const PokemonMap = ({
+ className,
+ showNorthArea,
+ northAreaName,
+ northAreaIcon,
+ northAreaColor,
+ showSouthArea,
+ southAreaName,
+ southAreaIcon,
+ southAreaColor,
+ showEastArea,
+ eastAreaName,
+ eastAreaIcon,
+ eastAreaColor,
+ showWestArea,
+ westAreaName,
+ westAreaIcon,
+ westAreaColor,
+ showCenterArea,
+ centerAreaName,
+ centerAreaIcon,
+ centerAreaColor
+}: IPokemonMap) => {
+ return (
+
+ {/* Empty top-left */}
+
+
+ {/* North area */}
+
+ );
+};
+
+// ๐ป 1D - Look at how verbose these prop combinations are!
+// ๐ป 1E - Refactor these to use slots: northSlot={}, centerSlot={}, etc.
+export const Exercise = () => (
+
+);
\ No newline at end of file
diff --git a/src/course/02- lessons/11-Slots/icons/index.tsx b/src/course/02-lessons/01-Bronze/Slots/icons/index.tsx
similarity index 100%
rename from src/course/02- lessons/11-Slots/icons/index.tsx
rename to src/course/02-lessons/01-Bronze/Slots/icons/index.tsx
diff --git a/src/course/02-lessons/01-Bronze/Slots/lesson.mdx b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx
new file mode 100644
index 0000000..13517c4
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/Slots/lesson.mdx
@@ -0,0 +1,73 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฐ Slots Pattern
+
+The slots pattern is very similar to the [render props pattern](?path=/docs/lessons-03-render-props-pattern-01-lesson--docs) but it has a slight difference which justifies why you would do one over the other.
+
+Here is an example of how you do a render prop pattern:
+
+```jsx
+const MapArea = ({ render }) => render({ area: 'forest', pokemonCount: 3 });
+
+// How you would use it.
+
{area}: {pokemonCount} Pokemon
} />;
+```
+
+The key thing here is that we are passing some state from our react component which can be used with components outside. What if we didn't need the area data? We could still use this pattern but an alternative would be a slot.
+
+## Children is a slot & a render prop
+
+Now the easiest way to form a connection with the wording of "slot" is to think of children. The children prop in React can either be a slot of a render prop in react.
+
+```
+{children} // slot
+{children({ area: 'forest', pokemonCount: 3 })} // render prop
+```
+
+## Using slots as props
+
+Here is an example of us using a Pokemon map layout with slots.
+
+```jsx
+const PokemonMap = ({ northSlot, southSlot, eastSlot, westSlot, centerSlot }) => (
+
+
{northSlot}
+
{westSlot}
+
{centerSlot}
+
{eastSlot}
+
{southSlot}
+
+);
+
+// How you would use it.
+๐ฒ Forest
}
+ centerSlot={
๐ Pallet Town
}
+ southSlot={
๐ Route 1
+ eastSlot={
โก Power Plant
}
+ westSlot={
๐๏ธ Mt. Silver
}
+/>;
+```
+
+The pros to using this approach are:
+
+- Useful for layout components where you make the responsibility of the layout purely slot-based and port different areas/locations using slots.
+- Removing many if statements and prop drilling from your presentational component
+- Flexible map layouts for different regions (Kanto, Johto, etc.)
+- Easy to swap out different locations without changing the map structure
+
+The cons of using this approach:
+
+- If you need to use the same UI in multiple places you will duplicate code (can be solved by making an abstraction component)
+
+## Exercise
+
+So in the exercise we have a task to create a Pokemon world map layout component. The Pokemon world needs to display different locations in specific positions: towns in the center, routes connecting them, and special areas like forests and mountains. Our task is to implement this using slots so that any location can be placed in any position on the map.
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx
new file mode 100644
index 0000000..ef12031
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Form.tsx
@@ -0,0 +1,111 @@
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonTypesApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+import classNames from 'classnames';
+import { FormEvent } from 'react';
+
+// ๐ง๐ปโ๐ป 1.f: we need to pass a prop called onPokemonTypesUpdate which will take a string[] setup the interface and suppky the parameter to the Form component.
+
+export const Form = () => {
+ // ๐ง๐ปโ๐ป 1.c: Setup a useState state colocation variable called selectedPokemonTypes, setSelectedPokemonTypes which will have a default of []
+
+ // โ๐ป This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex
+ const { data, isLoading } = usePokedex({
+ path: 'types',
+ queryParams: 'pageSize=8'
+ });
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+
+ // ๐ง๐ปโ๐ป 1.g: now we want to validate whether the selectedPokemonTypes have 4 items in the array before we call onPokemonTypesUpdate(selectedPokemonTypes).
+
+ // Once completed, head over to Screen.tsx as the Form component will be complaining about a missing prop.
+ };
+
+ const onPokemonTypeSelection = (type: string) => {
+ // ๐ฃ We can get rid of this line once we start using the type param.
+ console.log(type);
+ // ๐ง๐ปโ๐ป 1.e: we need to check IF the selectedPokemonTypes already has the selectedType
+ // because we need to toggle it on and off. If it is selected, we just setSelectedPokemonTypes with the filtered out type
+ // if it's not in there then we set the type [...selectedPokemonTypes, type];
+ };
+
+ return (
+
+
+ Select you favorite pokemon types (max 4)
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx
new file mode 100644
index 0000000..e2c27c5
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/PokemonOptions.tsx
@@ -0,0 +1,129 @@
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonCardsApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+import classNames from 'classnames';
+import { FormEvent } from 'react';
+
+interface IPokemonOptions {
+ type: string;
+ // ๐ง๐ปโ๐ป 2.h: Add two new props called onPokemonSelection which takes a string[] and string as params and another
+ // variable called defaultSelectedPokemon which is an optional string[].
+}
+
+export const PokemonOptions = ({ type }: IPokemonOptions) => {
+ // ๐ง๐ปโ๐ป 2.d: Create a selectedPokemon useState variable. Default value to be []
+
+ // ๐ง๐ปโ๐ป 2.i: Replace the default of selectedPokemon from [] to the defaultSelectedPokemon. This will remember which cards were selected when the component re-renders.
+
+ // โ๐ป This is already done for you. Feel free to have a look how it works in shared/hooks/usePokedex
+ const { data, isLoading, isError } = usePokedex<
+ TPokemonCardsApiResponse[]
+ >({
+ path: 'cards',
+ queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`,
+ skip: type === undefined
+ });
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+
+ // ๐ง๐ปโ๐ป 2.j: call onPokemonSelection(selectedPokemon, type);
+ };
+
+ const togglePokemonSelection = (pokemonId: string) => {
+ // ๐ฃ We can get rid of this line once we start using the type param.
+ console.log(pokemonId);
+ // ๐ง๐ปโ๐ป 2.g: We need to now update the state for when a pokemon card is selected or not.
+ // IF selectedPokemon includes the pokemonId then we need to de-select that pokemon card.
+ // ELSE we add the pokemon id to the array of pokemon cards [...selectedPokemon, pokemonId] and then setSelectedPokemon(newValues) (make this a variable as we will need it for later.)
+ // You should now start to see the pokemon being selected and de-selected. But the next thing we need to do is update the state within the screen. Search for 2.h
+ // ๐ง๐ปโ๐ป 2.k: Inside the ELSE, check if the newlySelectedPokemon has the length of 2. IF it does, call onPokemonSelection(newlySelectedPokemon, type);. Head over to the Screen.tsx component to finish it off.
+ };
+
+ return (
+
+
+ {type} Pokemon
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx
new file mode 100644
index 0000000..62d6c72
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/components/Screen.tsx
@@ -0,0 +1,84 @@
+/**
+ * Exercise: ๐งผ State Colocation vs ๐ช State Lifting
+ *
+ * ๐ค Observations of this file
+ * So the lead developer wanted us to manage the state for the selected pokemon types & the selected pokemon at this level so those variables can be used here and within the children. Each variable will be an array of strings which represent the type or the id of the selected pokemon.
+ *
+ * We need to tackle this in stages...
+ *
+ * Stage one (follow 1.* steps) - Creating the form that returns the pokemon types and saving those selected types to the lifted state variable
+ * Stage two (following 2.* steps) - Using those types, we will render the pokemon options component
+ *
+ */
+
+export const Screen = () => {
+ // ๐ง๐ปโ๐ป 1.a: Create a useState variable called selectedPokemonTypes, setSelectedPokemonTypes. Default to be an empty array.
+
+ // ๐ง๐ปโ๐ป 2.a: Create a useState> variable called selectedPokemon, setSelectedPokemon. Default to be an object {}.
+
+ // ๐ง๐ปโ๐ป 1.h: Create a function called onPokemonTypesUpdate which will take a types: string[] param. Pass that into the Form component as a prop. The function will just need setSelectedPokemonTypes for now.
+ // ๐ STAGE ONE COMPLETED you should now be able to see the types display, select them and then the state in the screen gets updated.
+
+ // ๐ง๐ปโ๐ป 2.m: You will now start to see the happy path all working fine, however when you change to different pokemon types and receive a new set of pokemon you now get some messy state where selectedPokemon is more than 8. To fix this, write the following code inside 1.h function (before the setSelectedPokemonTypes)
+ /*
+ const newlyUpdatedPokemon = { ...selectedPokemon };
+
+ selectedPokemonTypes
+ .filter((type) => {
+ return !types.find((selectedType) => selectedType === type);
+ })
+ .forEach((type) => {
+ delete newlyUpdatedPokemon[type];
+ });
+
+ setSelectedPokemon(newlyUpdatedPokemon);
+ */
+ // STAGE TWO completed. You have now built the screen. BUT ๐ there is a bug where the PokemonOptions re-renders the types that did not need to update when you change your types after one try. The reason is the "key" prop using index. The api has no identifier per type. If you enjoyed this exercise have a look into fixing it and make a pr.
+
+ // ๐ง๐ปโ๐ป 2.l: Create a function called onPokemonSelection which will take a pokemon: string[], type: string
+ // Then create a newlySelectedPokemon variable which will be a copy of the current selectedPokemon {...selectedPokemon}
+ // assign the newlySelectedPokemon[type] to equal the pokemon variable.
+ // setSelectedPokemon(newlySelectedPokemon);
+
+ return (
+
+
+
+
+
+
+
+
+ Battle Picker
+
+
+
+
+ {/* ๐ง๐ปโ๐ป 1.b: Render pokemon types form from ./components/Form and then head over to the form component */}
+
+
+ {/* ๐ง๐ปโ๐ป 2.b: Loop through the selectedPokemonTypes and pass down the type property to the PokemonOptions (./components/PokemonOptions) component. You will also need a key to be on the component. I used `${pokemonType}-${index}` */}
+
+
+ {/* ๐ง๐ปโ๐ป 2.c: We need to check if the KEYS in the selectedPokemon object equal 4 and the selectedPokemonTypes length is 4 before rendering the code snippet below. Head over to PokemonOptions when completed. */}
+ {/*
+
+ */}
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..b808a12
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title:
+ 'Lessons/๐ฅ Bronze/๐งผ State Colocation vs ๐ช State Lifting/02-Exercise',
+ component: Exercise,
+ parameters: {
+ layout: 'fullscreen'
+ }
+};
+
+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/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx
new file mode 100644
index 0000000..ee28457
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/exercise/exercise.tsx
@@ -0,0 +1,4 @@
+import { Screen } from './components/Screen';
+
+// Head over to screen to get started.
+export const Exercise = () => ;
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx
new file mode 100644
index 0000000..1f46ed8
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Form.tsx
@@ -0,0 +1,102 @@
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonTypesApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+import classNames from 'classnames';
+import { FormEvent, useState } from 'react';
+
+interface IForm {
+ onPokemonTypesUpdate: (types: string[]) => void;
+}
+
+export const Form = ({ onPokemonTypesUpdate }: IForm) => {
+ const [selectedPokemonTypes, setSelectedPokemonTypes] = useState<
+ Set
+ >(new Set());
+
+ const { data, isLoading } = usePokedex({
+ path: 'types',
+ queryParams: 'pageSize=8'
+ });
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+
+ if (selectedPokemonTypes.size === 4) {
+ onPokemonTypesUpdate(Array.from(selectedPokemonTypes));
+ }
+ };
+
+ const onPokemonTypeSelection = (type: string) => {
+ const newTypes = new Set(selectedPokemonTypes);
+ if (newTypes.has(type)) {
+ newTypes.delete(type);
+ } else {
+ newTypes.add(type);
+ }
+ setSelectedPokemonTypes(newTypes);
+ };
+
+ return (
+
+
+ Select you favorite pokemon types (max 4)
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx
new file mode 100644
index 0000000..cb2c8e5
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/PokemonOptions.tsx
@@ -0,0 +1,141 @@
+import { Skeleton } from '@shared/components/Skeleton/Skeleton.component';
+import {
+ TPokemonCardsApiResponse,
+ usePokedex
+} from '@shared/hooks/usePokedex';
+import classNames from 'classnames';
+import { FormEvent, useState } from 'react';
+
+interface IPokemonOptions {
+ type: string;
+ onPokemonSelection: (pokemonIds: string[], type: string) => void;
+ defaultSelectedPokemon?: string[];
+}
+
+export const PokemonOptions = ({
+ type,
+ onPokemonSelection,
+ defaultSelectedPokemon = []
+}: IPokemonOptions) => {
+ // ๐งผ State Colocation: we only want to have 2 cards in each component selected and use that for validation.
+ const [selectedPokemon, setSelectedPokemon] = useState(
+ // ๐ช State Lifting: We inherit the lifted state to maintain the selected options when we change pokemon types.
+ defaultSelectedPokemon
+ );
+
+ // ๐งผ State Colocation: We want to only call this api for the type provided and not all the types selected.
+ const { data, isLoading, isError } = usePokedex<
+ TPokemonCardsApiResponse[]
+ >({
+ path: 'cards',
+ queryParams: `pageSize=4&q=types:${type}&supertype:pokemon`,
+ skip: type === undefined
+ });
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+
+ // ๐ช State Lifting passing a function in as a prop to update the state above.
+ onPokemonSelection(selectedPokemon, type);
+ };
+
+ const togglePokemonSelection = (pokemonId: string) => {
+ if (selectedPokemon.includes(pokemonId)) {
+ setSelectedPokemon(
+ selectedPokemon.filter(
+ (selectedPokemonId) => selectedPokemonId !== pokemonId
+ )
+ );
+ } else {
+ const newlySelectedPokemon = [...selectedPokemon, pokemonId];
+ // ๐งผ State Colocation: Updating the visual state of the selected pokemon.
+ setSelectedPokemon(newlySelectedPokemon);
+
+ if (newlySelectedPokemon.length === 2) {
+ // ๐ช State Lifting passing a function in as a prop to update the state above.
+ onPokemonSelection(newlySelectedPokemon, type);
+ }
+ }
+ };
+
+ return (
+
+
+ {type} Pokemon
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx
new file mode 100644
index 0000000..56a01b5
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/components/Screen.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { Form } from './Form';
+import { PokemonOptions } from './PokemonOptions';
+
+export const Screen = () => {
+ // ๐ช State Lifting: Setting up shared state variables so the components in this scope can read and use them
+ const [selectedPokemonTypes, setSelectedPokemonTypes] = useState<
+ string[]
+ >([]);
+
+ // ๐ช State Lifting: Setting up shared state variables so the components in this scope can read and use them
+ const [selectedPokemon, setSelectedPokemon] = useState<
+ Record
+ >({});
+
+ // ๐ช State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component.
+ const onPokemonTypesUpdate = (types: string[]) => {
+ const newlyUpdatedPokemon = { ...selectedPokemon };
+ const typesSet = new Set(types);
+
+ // Remove types that are no longer selected
+ selectedPokemonTypes.forEach((type) => {
+ if (!typesSet.has(type)) {
+ delete newlyUpdatedPokemon[type];
+ }
+ });
+
+ setSelectedPokemon(newlyUpdatedPokemon);
+ setSelectedPokemonTypes(types);
+ };
+
+ // ๐ช State Lifting: Setting up a function which will update the state instead of bleeding this complexity into the UI component.
+ const onPokemonSelection = (pokemon: string[], type: string) => {
+ const newPokemon = { ...selectedPokemon };
+ newPokemon[type] = pokemon;
+ setSelectedPokemon(newPokemon);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx
new file mode 100644
index 0000000..0b0f87b
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/final.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Final } from './final';
+
+const meta: Meta = {
+ title:
+ 'Lessons/๐ฅ Bronze/๐งผ State Colocation vs ๐ช State Lifting/03-Final',
+ component: Final,
+ parameters: {
+ layout: 'fullscreen'
+ }
+};
+
+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/01-Bronze/StateColocationVsStateLifting/final/final.tsx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx
new file mode 100644
index 0000000..0bcdfa9
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/final/final.tsx
@@ -0,0 +1,3 @@
+import { Screen } from './components/Screen';
+
+export const Final = () => ;
diff --git a/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx
new file mode 100644
index 0000000..79ff880
--- /dev/null
+++ b/src/course/02-lessons/01-Bronze/StateColocationVsStateLifting/lesson.mdx
@@ -0,0 +1,123 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+---
+
+# ๐งผ State Colocation vs ๐ช State Lifting
+
+Understanding the difference between **state colocation** and **state lifting** is crucial for managing state effectively in React apps. Although they both deal with the _placement_ of state, they serve **opposite purposes**.
+
+---
+
+## ๐งผ State Colocation
+
+### What is it?
+
+Keep state **as close as possible** to where itโs needed.
+
+```jsx
+function SearchInput() {
+ const [query, setQuery] = useState(''); // colocated inside the component that needs it
+
+ return (
+ setQuery(e.target.value)} />
+ );
+}
+```
+
+### What It Means:
+
+Only the component that directly uses the state should own it. This minimizes prop drilling and makes the component more self-contained.
+
+### When to Use
+
+- Only one component uses the state
+- No need to share the state with siblings or parents
+
+### Benefits
+
+- Easier to maintain and test
+- Reduces unnecessary props
+- Keeps state logically scoped
+
+---
+
+## ๐ช State Lifting
+
+### What is it?
+
+Move state **up** the component tree to share it across multiple components.
+
+```jsx
+function Parent() {
+ const [count, setCount] = useState(0); // lifted up here
+
+ return (
+ <>
+ setCount(count + 1)}
+ />
+
+ >
+ );
+}
+```
+
+### What It Means
+
+If two or more components need access to or control over the same piece of state, lift it to their **closest common ancestor**.
+
+### When to Use
+
+- Two or more components need to read from or update the same state
+- You need synchronized behavior across sibling components
+
+### Benefits
+
+- Keeps state in sync
+- Prevents duplication or divergence of values
+- Encourages better separation of concerns
+
+---
+
+## ๐ง Summary Table
+
+| Pattern | Goal | Where State Lives | When to Use |
+| -------------------- | ------------------------------------------ | ----------------------- | --------------------------------------- |
+| **State Colocation** | Keep state near the component that uses it | Inside the component | When only one component needs the state |
+| **State Lifting** | Share state across components | Common parent component | When multiple components need the state |
+
+---
+
+## ๐ง Rule of Thumb
+
+- Use **colocation** by default.
+- Use **lifting** when you need to **share or sync state** between components.
+
+---
+
+## Exercise
+
+### Scenario
+
+You are creating a part of the new Pokemon Battle game and the screen you are working on is the "Pick your team" screen. We need to be able to choose up to four pokemon types and then choose 2 randomly picked pokemon from each type before we move onto the battle screen. The screen will work like this:
+
+1. You select four options from the form and select get Pokemon.
+2. You will then be presented a grid which will display four different Pokemon per type.
+3. You select two pokemon from each type to be your chosen pokemon for the battle
+4. You click Ready to battle.
+
+### What we are going to do?
+
+The lead developer on the team has structured how the screen should work:
+
+- **components/Screen** - manages the state selectedPokemonTypes & selectedPokemonForBattle and handles the updating of those variables.
+- **components/Form** - fetches all the pokemon types and displays a form where the player can select up to four types and then click get pokemon to update the screens state.
+- **components/PokemonOptions** - fetches the pokemon for the type it has been given, handles the selection of only 2 in each group and then updates the screens state.
+- **shared/hooks/usePokedex** - There is a reuseable react hook that we can already use in the form/pokemon options to display the data that we want to display. The pokedex has two apis known as "types" and "cards" api.
+
+## Issues
+
+Any issues or improvements to the course please raise [here](https://github.com/code-mattclaffey/react-design-patterns/issues/new).
diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx
new file mode 100644
index 0000000..18e33c4
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Compound/exercise/components/PokemonTeamBuilder.tsx
@@ -0,0 +1,94 @@
+import classNames from 'classnames';
+
+interface IPokemonTeamBuilder {
+ title: string;
+ children: React.ReactNode;
+}
+
+interface ITeamSlot {
+ position: number;
+ pokemonName?: string;
+ pokemonLevel?: number;
+ pokemonType?: string;
+ isSelected: boolean;
+ slotId: string;
+ onClick: () => void;
+ onSelect: () => void;
+}
+
+// 1B Move the useState from exercise.tsx here and manage the state internally
+// 1C Create a React Context to share state between PokemonTeamBuilder and TeamSlot
+// 1D Remove isSelected, slotId, onClick, onSelect from ITeamSlot interface
+export const PokemonTeamBuilder = ({
+ title,
+ children
+}: IPokemonTeamBuilder) => {
+ return (
+
+
+ {title}
+
+
+ {children}
+
+
+ );
+};
+
+// 1E Update this component to use useContext to get state instead of direct props
+export const TeamSlot = ({
+ position,
+ pokemonName,
+ pokemonLevel,
+ pokemonType,
+ isSelected,
+ onClick,
+ onSelect
+}: ITeamSlot) => {
+ return (
+
+
+
+ Slot {position}
+
+
+ {pokemonName ? (
+
+
+ {pokemonName}
+
+
+ Level {pokemonLevel}
+
+
+ {pokemonType}
+
+
+ ) : (
+
+
โ
+
Empty Slot
+
+ )}
+
+
+ );
+};
+
+// ๐ป 1F export const PokemonTeam which will be an object containing the PokemonTeamBuilder and TeamSlot components
+// Tip: export const ComponentName = Object.assign(ParentComponent, { MyComponent: Component });
+export const PokemonTeam = Object.assign(PokemonTeamBuilder, {
+ Slot: TeamSlot
+});
diff --git a/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..d4f8ce4
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Silver/๐งฉ Compound Components Pattern/02-Exercise',
+ component: Exercise
+};
+
+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/02-Silver/Compound/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx
new file mode 100644
index 0000000..3326f4e
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Compound/exercise/exercise.tsx
@@ -0,0 +1,84 @@
+import { useState } from 'react';
+import {
+ PokemonTeamBuilder,
+ TeamSlot
+} from './components/PokemonTeamBuilder';
+
+/**
+ * Exercise: Convert the current Pokemon team builder implementation to use the compound pattern
+ *
+ * ๐ค Observations of this file
+ * As you can see in this component we have some useState which is managing which team slot is selected at any given time. We need to move this logic into the PokemonTeamBuilder component and pass down the props into the TeamSlot that way instead of managing it here in this file.
+ *
+ */
+
+// ๐ป 1A Copy the useState on line 16 and go to ./components/PokemonTeamBuilder.tsx
+export const Exercise = () => {
+ // ๐ฃ 2A Remove the useState and the isSelected, slotId, onClick, onSelect props from all the TeamSlots
+
+ const [selectedSlot, setSelectedSlot] = useState();
+
+ // ๐ป 2B Import PokemonTeam from ./components/PokemonTeamBuilder.tsx
+ // Change PokemonTeamBuilder to PokemonTeam and change TeamSlot to PokemonTeam.Slot
+
+ return (
+
+);
diff --git a/src/course/02-lessons/02-Silver/Compound/lesson.mdx b/src/course/02-lessons/02-Silver/Compound/lesson.mdx
new file mode 100644
index 0000000..ec92035
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Compound/lesson.mdx
@@ -0,0 +1,47 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐งฉ Compound Components Pattern
+
+Compound components is an advanced React container pattern that provides a simple and efficient way for multiple components to share states and handle logic โ working together.
+
+The compound components pattern provides an expressive and flexible API for communication between a parent component and its children. Also, the compound components pattern enables a parent component to interact and share state with its children implicitly, which makes it suitable for building declarative UI. A good example is a Pokemon team builder:
+
+```jsx
+
+
+
+
+
+
+
+
+```
+
+In the code above, the PokemonTeam component manages and shares its state implicitly with the Slot and Pokemon components. Consequently, although there is no explicit state declaration, the team knows which Pokemon are selected and their positions.
+
+The compound component pattern is useful in building complex React components such as team builders, accordions, tab switchers, dropdowns, and more. It can be implemented either by using the Context API or the React.cloneElement function.
+
+## Exercise
+
+A requirement has come in to reuse the Pokemon team builder in another location of our application. The current implementation of the team builder has its state management implemented only on the page that this component is used on. We need to refactor the component to use the compound design pattern so that it can be re-used on both pages. Head over to the exercise.tsx to continue.
+
+## When to use this pattern?
+
+**Use compound components for:**
+- **Complex UI**: Multi-part components like accordions, tabs, modals
+- **Flexible API**: When you want declarative, composable interfaces
+- **State Sharing**: Components that need to share state implicitly
+- **Reusable Libraries**: Building component libraries with flexible APIs
+
+**Avoid when:**
+- **Simple Components**: Basic components don't need compound patterns
+- **Performance**: Can add complexity if not needed
+- **Learning Curve**: May be confusing for junior developers
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..e9e36b5
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title:
+ 'Lessons/๐ฅ Silver/๐ฎ Controlled Components Pattern/02-Exercise',
+ component: Exercise
+};
+
+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/02-Silver/Controlled/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx
new file mode 100644
index 0000000..912c6a8
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx
@@ -0,0 +1,214 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import classNames from 'classnames';
+// ๐ฃ You can get rid of this eslint error comment when finished.
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { useEffect, useRef, useState } from 'react';
+import FocusLock from 'react-focus-lock';
+import { Button } from '@shared/components/Button/Button.component';
+
+interface IEvolutionModal {
+ isVisible: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ id: string;
+ pokemon: {
+ name: string;
+ level: number;
+ currentSprite: string;
+ };
+ evolution: {
+ name: string;
+ sprite: string;
+ requirement: string;
+ };
+}
+
+// For the full guide to making an accessible modal you can follow below to get every instance
+// โฟ๏ธ WCAG Modal Resource: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/
+// ๐ฃ You can get rid of this eslint error comment when finished.
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const EvolutionModal = ({
+ isVisible,
+ // ๐ฃ You can get rid of this eslint error comment when finished.
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ onClose,
+ // ๐ฃ You can get rid of this eslint error comment when finished.
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ onConfirm,
+ id,
+ pokemon,
+ evolution
+}: IEvolutionModal) => {
+ // 2a ๐ป Create a useRef and bind the ref to the div on line 70
+
+ useEffect(() => {
+ // โ๐ป When a modal is visible you want to navigate the focus from
+ // the actioner (what caused the modal to open) to the content
+ // โฟ๏ธ It helps the screenreader not get lost on the page
+ // 2b - ๐ป Check if isVisible is true and the modal.current is defined before setting focus to the modal
+ }, [isVisible]);
+
+ // ๐ฃ You can get rid of this eslint error comment when finished.
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const onModalPress = (event: React.MouseEvent) => {
+ // You may have noticed how we have added an onClose event to the container
+ // and that is because customers normally click out of the modal to leave,
+ // but if they click within the modal happens. The event "bubbles" up to
+ // the container div and it closes the modal. Which is janky behaviour
+ // ๐งช When you finish, remove the onModal press from the modal and try to click inside the modal then add it back
+ // โ๐ป Stop propagation prevents an event from bubbling to the top.
+ // โ๐ป https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ return (
+
+
+ {/* โ๐ป SUPER important for meeting the WCAG quidelines is that you need focus, but locked within this div */}
+ {/* When focus is landed in this box with a keyboard you can no longer get out so make sure you have a close button */}
+ {/* โฟ๏ธ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! ๐ฆธ๐ปโ๏ธ */}
+
+
+ {/* 2f - ๐ป โฟ๏ธ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */}
+
+ โจ Evolution Time! โจ
+
+
+
+
+
+
+
{pokemon.name}
+
+ Level {pokemon.level}
+
+
+
+
โ
+
+
+
+
{evolution.name}
+
+ {evolution.requirement}
+
+
+
+
+
+
+ {/* 2g - ๐ป Add onClick={onConfirm} for evolution confirmation */}
+
+ {/* 2h - ๐ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */}
+
+
+
+ {/* 2i - ๐ป โฟ๏ธ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */}
+
+ Your {pokemon.name} is ready to evolve into{' '}
+ {evolution.name}!
+
+
+
+
+ );
+};
+
+export const Exercise = () => {
+ // 1a ๐ป Create a state hook variable with isEvolutionVisible and setIsEvolutionVisible
+
+ // 1b ๐ป Create an onClose event that sets isEvolutionVisible to false
+
+ // 1c ๐ป Create an onConfirm event that handles evolution and closes modal
+
+ // 1d ๐ป Create an onCheckEvolution event that sets isEvolutionVisible to true
+
+ const pokemon = {
+ name: 'Charmander',
+ level: 16,
+ currentSprite:
+ 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png'
+ };
+
+ // ๐ฃ You can get rid of this eslint error comment when finished.
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const evolution = {
+ name: 'Charmeleon',
+ sprite:
+ 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/5.png',
+ requirement: 'Level 16 reached!'
+ };
+
+ return (
+
+
+ ๐ฎ Pokemon Evolution System
+
+
+
+
+
{pokemon.name}
+
Level {pokemon.level}
+
+ Ready to evolve! ๐
+
+
+
+ {/* 1e ๐ป Add the onClick={onCheckEvolution} event to the button
+ โ๐ป This is an example of a Controlled component but in a Pokemon context.
+ As a developer, we are providing the button with those props for the button
+ to behave how we want it to behave, otherwise, it does nothing. */}
+
+
+
+
+ {/* 1f ๐ป Check if isEvolutionVisible (๐ Conditional Render Pattern) to render the EvolutionModal */}
+ {/* Map the isVisible, onClose, onConfirm props to the EvolutionModal. The other props can be whatever you want */}
+
+ );
+};
diff --git a/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx b/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx
new file mode 100644
index 0000000..ea63a77
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Controlled/final/final.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Final } from './final';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Silver/๐ฎ Controlled Components Pattern/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/02-Silver/Controlled/final/final.tsx b/src/course/02-lessons/02-Silver/Controlled/final/final.tsx
new file mode 100644
index 0000000..fff78c7
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Controlled/final/final.tsx
@@ -0,0 +1,218 @@
+import classNames from 'classnames';
+import { useEffect, useRef, useState } from 'react';
+import FocusLock from 'react-focus-lock';
+import { Button } from '@shared/components/Button/Button.component';
+
+interface IEvolutionModal {
+ isVisible: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ id: string;
+ pokemon: {
+ name: string;
+ level: number;
+ currentSprite: string;
+ };
+ evolution: {
+ name: string;
+ sprite: string;
+ requirement: string;
+ };
+}
+
+const EvolutionModal = ({
+ isVisible,
+ onClose,
+ onConfirm,
+ id,
+ pokemon,
+ evolution
+}: IEvolutionModal) => {
+ const modal = useRef(null);
+
+ useEffect(() => {
+ if (isVisible && modal.current) {
+ modal.current.focus();
+ }
+ }, [isVisible]);
+
+ const onModalPress = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ return (
+
+
+
+
+
+ โจ Evolution Time! โจ
+
+
+
+
+
+
+
{pokemon.name}
+
+ Level {pokemon.level}
+
+
+
+
โ
+
+
+
+
{evolution.name}
+
+ {evolution.requirement}
+
+
+
+
+
+
+
+
+
+
+
+ Your {pokemon.name} is ready to evolve into{' '}
+ {evolution.name}!
+
+ );
+};
diff --git a/src/course/02-lessons/02-Silver/Controlled/lesson.mdx b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx
new file mode 100644
index 0000000..a8cb896
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Controlled/lesson.mdx
@@ -0,0 +1,67 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฎ Controlled Components Pattern
+
+The concept of controlled components involves creating components with highly predictable behavior by managing their state through props. A controlled components behavior changes based on the state passed to it as a prop.
+
+In the example below, a Pokemon evolution modal's visibility is controlled by evolution conditions, and it accepts evolution callbacks. The parent component manages when evolution can occur based on level, stones, or friendship requirements.
+
+```jsx
+const EvolutionModal = ({ isOpen, pokemon, evolutionData, onConfirm, onCancel }) => {
+ return (
+
+
{pokemon.name} is ready to evolve!
+
Evolve into {evolutionData.name}?
+
+
+
+ );
+};
+
+const PokemonTrainer = () => {
+ const [showEvolution, setShowEvolution] = useState(false);
+ const [pokemon, setPokemon] = useState({ name: 'Charmander', level: 16 });
+
+ const checkEvolution = () => {
+ if (pokemon.level >= 16) {
+ setShowEvolution(true);
+ }
+ };
+
+ const confirmEvolution = () => {
+ setPokemon({ name: 'Charmeleon', level: pokemon.level });
+ setShowEvolution(false);
+ };
+
+ return (
+ <>
+
+ setShowEvolution(false)}
+ />
+ >
+ );
+};
+```
+
+This is perfect for evolution modals as they require external conditions (level, items, friendship) to be met before they appear. The parent manages all evolution logic and state.
+
+## Exercise
+
+In this exercise we are going to build a Pokemon Evolution Modal component which will be controlled via props.
+
+The task: The Pokemon trainer needs an evolution confirmation dialog that appears when their Pokemon meets evolution requirements. The modal should be controlled by the parent component managing evolution conditions.
+
+Head over to the exercise and let's get started.
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..144db71
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Silver/๐ฏ FACC Pattern/Exercise',
+ component: Exercise,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx
new file mode 100644
index 0000000..e0e670a
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/FACC/exercise/exercise.tsx
@@ -0,0 +1,195 @@
+import { useState } from 'react';
+
+interface IPokemon {
+ name: string;
+ hp: number;
+ maxHp: number;
+ attack: number;
+}
+
+interface IBattleState {
+ playerPokemon: IPokemon;
+ enemyPokemon: IPokemon;
+ turn: 'player' | 'enemy';
+ battleLog: string[];
+ winner: string | null;
+}
+
+/*
+ * Observations
+ * ๐ Battle logic is tightly coupled with specific battle display
+ * The UI is hardcoded within the battle simulator
+
+ * Tasks
+ * 1A ๐ป - Refactor to use FACC pattern by adding children prop:
+ * children: (battleState: IBattleState, actions: IBattleActions) => React.ReactNode;
+ *
+ * 1B ๐ป - Create IBattleActions interface with:
+ * attack: () => void;
+ * resetBattle: () => void;
+ *
+ * 1C ๐ป - Replace the JSX return with children function call
+ * 1D ๐ป - In Exercise component, use FACC to render battle display
+*/
+
+export const PokemonBattleSimulator = () => {
+ const [battleState, setBattleState] = useState({
+ playerPokemon: {
+ name: 'Charizard',
+ hp: 100,
+ maxHp: 100,
+ attack: 25
+ },
+ enemyPokemon: {
+ name: 'Blastoise',
+ hp: 100,
+ maxHp: 100,
+ attack: 20
+ },
+ turn: 'player',
+ battleLog: [],
+ winner: null
+ });
+
+ const attack = () => {
+ if (battleState.winner) return;
+
+ setBattleState((prev) => {
+ const newState = { ...prev };
+
+ if (prev.turn === 'player') {
+ const damage = prev.playerPokemon.attack;
+ newState.enemyPokemon.hp = Math.max(
+ 0,
+ prev.enemyPokemon.hp - damage
+ );
+ newState.battleLog = [
+ ...prev.battleLog,
+ `${prev.playerPokemon.name} attacks for ${damage} damage!`
+ ];
+
+ if (newState.enemyPokemon.hp === 0) {
+ newState.winner = prev.playerPokemon.name;
+ } else {
+ newState.turn = 'enemy';
+ }
+ } else {
+ const damage = prev.enemyPokemon.attack;
+ newState.playerPokemon.hp = Math.max(
+ 0,
+ prev.playerPokemon.hp - damage
+ );
+ newState.battleLog = [
+ ...prev.battleLog,
+ `${prev.enemyPokemon.name} attacks for ${damage} damage!`
+ ];
+
+ if (newState.playerPokemon.hp === 0) {
+ newState.winner = prev.enemyPokemon.name;
+ } else {
+ newState.turn = 'player';
+ }
+ }
+
+ return newState;
+ });
+ };
+
+ const resetBattle = () => {
+ setBattleState({
+ playerPokemon: {
+ name: 'Charizard',
+ hp: 100,
+ maxHp: 100,
+ attack: 25
+ },
+ enemyPokemon: {
+ name: 'Blastoise',
+ hp: 100,
+ maxHp: 100,
+ attack: 20
+ },
+ turn: 'player',
+ battleLog: [],
+ winner: null
+ });
+ };
+
+ return (
+
+
Pokemon Battle
+
+
+
+
+ {battleState.playerPokemon.name}
+
+
+
+
+
+ {battleState.playerPokemon.hp}/
+ {battleState.playerPokemon.maxHp} HP
+
+
+
+
+
+ {battleState.enemyPokemon.name}
+
+
+
+
+
+ {battleState.enemyPokemon.hp}/
+ {battleState.enemyPokemon.maxHp} HP
+
+ );
+};
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/FACC/lesson.mdx b/src/course/02-lessons/02-Silver/FACC/lesson.mdx
new file mode 100644
index 0000000..ea24b0c
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/FACC/lesson.mdx
@@ -0,0 +1,48 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฏ FACC (Function as Child Component) Pattern
+
+FACC is a pattern where you pass a function as the children prop. The component calls this function with data, allowing complete control over rendering while sharing logic.
+
+## Basic Example
+
+```jsx
+const PokemonData = ({ children }) => {
+ const pokemon = { name: 'Pikachu', type: 'Electric' };
+ return children(pokemon);
+};
+
+// Usage
+
+ {(pokemon) => (
+
+ {pokemon.name} is {pokemon.type} type!
+
+ )}
+;
+```
+
+## Exercise
+
+In this exercise we have a Pokemon battle simulator that's tightly coupled with a specific battle display. Different teams want to use the battle logic but with their own custom battle interfaces.
+
+Your task is to refactor the battle simulator to use FACC pattern, separating the battle logic from the display components.
+
+Head over to the exercise file and let's begin.
+
+## Why use this pattern?
+
+FACC is valuable for:
+
+- **Clean API**: Uses the natural children prop instead of custom render props
+- **Flexible Rendering**: Complete control over how data is displayed
+- **Logic Reuse**: Share complex state management across different UIs
+- **Component Libraries**: Create headless components that work with any design system
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..b729d74
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Silver/๐ฆ Polymorphic Components Pattern/02-Exercise',
+ component: Exercise
+};
+
+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/02-Silver/PolymorphicComponents/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx
new file mode 100644
index 0000000..8841186
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/exercise/exercise.tsx
@@ -0,0 +1,146 @@
+import { HTMLAttributes } from 'react';
+
+/**
+ * Exercise: Refactor the StatusEffect component to correctly use the polymorphic pattern.
+ *
+ * ๐ค Observations of this file
+ * In the current component you can see that the as prop is a string so if a developer in a team uses the wrong element they would just get the span element.
+ * Status styles are clearly defined to the element so there is no flexibility in status effects which can lead to developers pleasing designers but... breaking accessibility or vice versa where designs do not look the same as what was provided.
+ *
+ * We need to tackle this in stages...
+ *
+ * Stage one - Refactoring the component to use Polymorphic style so we remove the switch statement.
+ * Stage two - decouple the status effect to the element
+ * Stage three - allow for developers to have a severity level for special status effects.
+ *
+ */
+
+// ๐ง๐ป๐ป 1.a - Create a type called AllowedElements for 'span' | 'div' | 'button' | 'li'
+
+// ๐ง๐ป๐ป 2.a - Create a type called StatusTypes and it's a union of 'normal' | 'burned' | 'poisoned' | 'paralyzed' | 'frozen' | 'asleep'
+
+interface IStatusEffect extends HTMLAttributes {
+ // ๐ง๐ป๐ป 1.b - Update the type of string to be the type you defined as part of 1.a
+ as?: string;
+ // ๐ง๐ป๐ป 2.b - Create a new prop called status?: StatusTypes;
+ // ๐ง๐ป๐ป 3.a - Create a new prop called severity?: 'mild' | 'severe';
+ children?: React.ReactNode | React.ReactNode[];
+}
+
+const StatusEffect = ({
+ // ๐ง๐ป๐ป 1.c - add : Element = 'span' what this will do is redefine the prop to be a capital variable which can be used as a React Component.
+ as = 'span',
+ // ๐ง๐ป๐ป 2.c - Create a new prop called status
+ // ๐ง๐ป๐ป 3.b - Create a new prop called severity
+ children,
+ ...rest
+}: IStatusEffect) => {
+ // ๐ง๐ป๐ป 1.d - Create a variable called statusClass which uses useMemo to return a string from an object key mapping. For example: useMemo(() => ({ span: 'text-gray-600' }[Element]), [Element]);
+ // ๐ง๐ป๐ป 2.d - In the useMemo add the status as a dependency and then check if status exists. If it does, return status-specific classes if not, return what was there previously. Move onto 3.a.
+
+ // ๐ง๐ป๐ป 3.c - create another useMemo for severityClass where we check if severity exists and return severity-specific classes.
+
+ // ๐งช 3.d Head down to the storybook Exercise Component and add a few more variants in.
+
+ // ๐ง๐ป๐ป 1.e return the Element with the className={classNames('px-2 py-1 rounded text-sm', statusClass)} don't forget the ...rest
+ // ๐ฃ 1.f remove the old code below. Move onto step 2.a.
+ if (as)
+ switch (as) {
+ case 'span':
+ return (
+
+ {children}
+
+ );
+ case 'div':
+ return (
+
+ {children}
+
+ );
+ case 'button':
+ return (
+
+ );
+ case 'li':
+ return (
+
+
+ {/* 3.e Implement a status effect as span with status burned */}
+
+ {/* 3.e Implement a status effect as div with status poisoned and severity severe */}
+
+ {/* 3.e Implement a status effect as button with severity mild */}
+
+);
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx
new file mode 100644
index 0000000..03a9983
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/PolymorphicComponents/lesson.mdx
@@ -0,0 +1,116 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฆ Polymorphic Components Pattern
+
+React excels at building reusable components, but repetition can creep in when components only differ slightlyโlike status effects that need different HTML elements based on context.
+
+Consider this Pokemon battle scenario:
+
+```jsx
+export const BurnedSpan = ({ children }) => (
+ {children}
+);
+export const ParalyzedButton = ({ children, onClick }) => (
+
+);
+export const PoisonedDiv = ({ children, turns }) => (
+
+);
+```
+
+Each of these components is nearly identical except for the HTML tag. This isn't scalable and can be difficult to maintain. The **polymorphic component pattern** solves this by allowing you to render different HTML elements using a single component, often via an **as** prop.
+
+## Who else does this?
+
+Most modern UI libraries (like Chakra UI or Radix UI) support polymorphic components. The **as** prop lets developers choose the HTML tag to render, giving flexibility while keeping styling and logic consistent.
+
+Example:
+
+```JSX
+
+ Burned - Click to heal
+
+
+
+ Paralyzed
+
+
+
+ Badly Poisoned
+
+```
+
+This would render as:
+
+```jsx
+
+Paralyzed
+
Badly Poisoned
+```
+
+## Implementation
+
+Here's a simple implementation of a polymorphic **StatusEffect** component:
+
+```jsx
+export const StatusEffect = ({
+ as: Component = 'span',
+ status = 'normal',
+ children,
+ ...props
+}) => {
+ return {children};
+};
+```
+
+> ๐ก **Tip:** When rendering dynamic elements in React, make sure your **as** value (e.g., **Component**) is capitalized or passed as a variable. React treats lowercase JSX tags as native HTML.
+
+## Why use this pattern?
+
+This pattern is extremely useful in design systems, especially when building:
+
+- Typography components (headings, paragraphs, labels)
+- Button variants (buttons, links, spans with click handlers)
+- Status indicators (badges, alerts, notifications)
+- Form elements that need semantic flexibility
+- Navigation components (links, buttons, divs)
+
+It ensures your components stay flexible while keeping semantic HTML and accessible markup intact.
+
+Here's a quick visual guide:
+
+| Usage | Renders As | Status Class |
+| ---------------------------------------- | ---------- | ------------ |
+| **StatusEffect** | **span** | **normal** |
+| **StatusEffect as="button"** | **button** | **normal** |
+| **StatusEffect status="burned"** | **span** | **burned** |
+| **StatusEffect as="div" status="poisoned"** | **div** | **poisoned** |
+
+## Exercise
+
+### Scenario
+
+The Pokemon battle system team is using a StatusEffect component and it works fairly well from an implementation point of view but there is often friction between the design teams and developers around design consistency vs accessibility.
+
+The previous developer built a status effect component using the right intentions however, the flexibility of the component is a little rigid.
+
+### What we are going to do?
+
+In today's exercise, we're going to refactor this component to use the Polymorphic pattern.
+
+It should:
+
+1. Render a semantic HTML tag based on the **as** prop BUT we have typescript in place to only allow for span, div, button, and li tags.
+2. Apply a CSS class based on a **status** prop (defaulting to 'normal' if **status** is not provided).
+3. Fall back to defaults (span, normal) if neither is provided.
+
+## Feedback
+
+Feedback is a gift and it helps me make these courses better for you. If you have 5 minutes, I'd love for you to fill out the feedback form:
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx
new file mode 100644
index 0000000..7bb98b1
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Portals/exercise/components/modal.tsx
@@ -0,0 +1,146 @@
+import classNames from 'classnames';
+import { useEffect, useRef } from 'react';
+// ๐ป 1B - import { createPortal } from 'react-dom';
+import FocusLock from 'react-focus-lock';
+import { Button } from '@shared/components/Button/Button.component';
+
+interface IBattleOverlay {
+ isVisible: boolean;
+ onClose: () => void;
+ id: string;
+ wildPokemon: {
+ name: string;
+ level: number;
+ sprite: string;
+ };
+ onBattleAction: (action: 'attack' | 'run') => void;
+ battleResult: 'won' | 'fled' | null;
+}
+
+export const BattleOverlay = ({
+ isVisible,
+ onClose,
+ id,
+ wildPokemon,
+ onBattleAction,
+ battleResult
+}: IBattleOverlay) => {
+ const modal = useRef(null);
+
+ useEffect(() => {
+ if (isVisible && modal.current) {
+ modal.current.focus();
+ }
+ }, [isVisible]);
+
+ const onModalPress = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ // ๐ป 1C - call createPortal(battleOverlayCode, document.body);
+ // ๐งช Test the storybook and look at how you can all of a sudden click the battle buttons
+ // This isn't saying the solution to override z-index is to use portal but more of the sense that if you need something
+ // put at the root of the DOM but do not wish to implement something extremely complex or app level then portal is handy for this.
+ return (
+
+
+
+
+
+ โ๏ธ Wild Pokemon Battle!
+
+
+ {!battleResult ? (
+
+
+
+
+ A wild {wildPokemon.name} appeared!
+
+
+ Level {wildPokemon.level}
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ {battleResult === 'won' ? (
+ <>
+
๐
+
+ Victory!
+
+
+ You defeated the wild {wildPokemon.name}!
+
+ >
+ ) : (
+ <>
+
๐จ
+
+ Escaped!
+
+
+ The wild {wildPokemon.name} got away!
+
+ >
+ )}
+
+
+ )}
+
+
+ {!battleResult &&
+ `What will you do against the wild ${wildPokemon.name}?`}
+
+
+
+
+ );
+};
diff --git a/src/course/02- lessons/10-Compound/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx
similarity index 87%
rename from src/course/02- lessons/10-Compound/exercise.stories.tsx
rename to src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx
index ac0daa2..c57ec29 100644
--- a/src/course/02- lessons/10-Compound/exercise.stories.tsx
+++ b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/10 - Compound Components Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Silver/๐ Portals Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx
new file mode 100644
index 0000000..68a05ab
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Portals/exercise/exercise.tsx
@@ -0,0 +1,116 @@
+import { useState } from 'react';
+import { BattleOverlay } from './components/modal';
+import { Button } from '@shared/components/Button/Button.component';
+
+// ๐ป 1A - have a look at the current implementation of the battle overlay and then go to components/modal.tsx
+
+export const Exercise = () => {
+ const [isBattleActive, setIsBattleActive] = useState(false);
+ const [battleResult, setBattleResult] = useState<
+ 'won' | 'fled' | null
+ >(null);
+
+ const onCloseBattle = () => {
+ setIsBattleActive(false);
+ };
+
+ const onStartBattle = () => {
+ setIsBattleActive(true);
+ setBattleResult(null);
+ };
+
+ const onBattleAction = (action: 'attack' | 'run') => {
+ if (action === 'attack') {
+ setBattleResult('won');
+ } else {
+ setBattleResult('fled');
+ }
+ setTimeout(() => {
+ setIsBattleActive(false);
+ setBattleResult(null);
+ }, 2000);
+ };
+
+ return (
+ // ๐งช We have z-index 10 on the section and then z-9998 on a div that's purposely there. Our BattleOverlay has a z-20 which means:
+ // section z-10
+ // battle overlay z-20 (but this means z-20 within the z-10) think of it as a sub layer.
+ // the bug is 9998 and a css hack for the battle buttons is 9999
+
+
+
+
+
+ ๐ฟ Pokemon World
+
+
+
+
+
๐ฒ
+
Tall Grass
+
+
+
๐
+
Pokemon Center
+
+
+
๐ช
+
Poke Mart
+
+
+
+
+
+ ๐ Trainer Actions
+
+
+ You're walking through the tall grass. Wild Pokemon might
+ appear!
+
+
+
+
+
+
+
+
+
๐ฎ Game Status
+
+ {isBattleActive
+ ? 'Battle in progress...'
+ : 'Exploring the world'}
+
+
+ {isBattleActive && (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/Portals/lesson.mdx b/src/course/02-lessons/02-Silver/Portals/lesson.mdx
new file mode 100644
index 0000000..7b534ff
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Portals/lesson.mdx
@@ -0,0 +1,61 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ Portals Pattern
+
+A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is perfect for Pokemon battle overlays that need to appear above everything else in the game.
+
+```jsx
+import { createPortal } from 'react-dom';
+
+// ...
+
+
+
This is the main game interface.
+ {createPortal(
+
+
A wild Pokemon appeared!
+
,
+ document.body
+ )}
+
;
+```
+
+Which in html, will translate to:
+
+```html
+
+
+ Pokemon Game
+
+
+
+
+
This is the main game interface.
+
+
+
+
+
A wild Pokemon appeared!
+
+
+
+```
+
+Portal benefits:
+
+- Battle overlays render above all game content
+- No clashes with z-index as the battle screen is at the root of the DOM
+- Simplified state management for battle transitions
+- Perfect for full-screen battle interfaces
+
+## Exercise
+
+In the current Pokemon game when a trainer encounters a wild Pokemon and the battle overlay appears, the trainer cannot interact with the battle interface properly due to z-index issues with the main game content.
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
\ No newline at end of file
diff --git a/src/course/02- lessons/08-Provider/Provider.tsx b/src/course/02-lessons/02-Silver/Provider/exercise/Provider.tsx
similarity index 100%
rename from src/course/02- lessons/08-Provider/Provider.tsx
rename to src/course/02-lessons/02-Silver/Provider/exercise/Provider.tsx
diff --git a/src/course/02- lessons/03-RenderProps/exercise.stories.tsx b/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx
similarity index 87%
rename from src/course/02- lessons/03-RenderProps/exercise.stories.tsx
rename to src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx
index b039a1f..d329d80 100644
--- a/src/course/02- lessons/03-RenderProps/exercise.stories.tsx
+++ b/src/course/02-lessons/02-Silver/Provider/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/03 - Render Props Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Silver/๐ Provider Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02- lessons/08-Provider/exercise.tsx b/src/course/02-lessons/02-Silver/Provider/exercise/exercise.tsx
similarity index 100%
rename from src/course/02- lessons/08-Provider/exercise.tsx
rename to src/course/02-lessons/02-Silver/Provider/exercise/exercise.tsx
diff --git a/src/course/02-solutions/08-Provider/Provider.tsx b/src/course/02-lessons/02-Silver/Provider/final/Provider.tsx
similarity index 95%
rename from src/course/02-solutions/08-Provider/Provider.tsx
rename to src/course/02-lessons/02-Silver/Provider/final/Provider.tsx
index 6f0bdb8..82dff67 100644
--- a/src/course/02-solutions/08-Provider/Provider.tsx
+++ b/src/course/02-lessons/02-Silver/Provider/final/Provider.tsx
@@ -2,7 +2,7 @@ import { createContext, useContext, useMemo, useState } from 'react';
import {
IPokemonManagerState,
PokemonManager
-} from '../../../shared/modules/PokemonManager/PokemonManager';
+} from '@shared/modules/PokemonManager/PokemonManager';
export interface IPokemonProviderState extends IPokemonManagerState {
fetchPokemons: (total: number) => Promise;
diff --git a/src/course/02-solutions/08-Provider/final.stories.tsx b/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx
similarity index 87%
rename from src/course/02-solutions/08-Provider/final.stories.tsx
rename to src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx
index e9d0334..6b9ad89 100644
--- a/src/course/02-solutions/08-Provider/final.stories.tsx
+++ b/src/course/02-lessons/02-Silver/Provider/final/final.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Final } from './final';
const meta: Meta = {
- title: 'Lessons/08 - Provider Pattern/03-Final',
+ title: 'Lessons/๐ฅ Silver/๐ Provider Pattern/03-Final',
component: Final
};
diff --git a/src/course/02-solutions/08-Provider/final.tsx b/src/course/02-lessons/02-Silver/Provider/final/final.tsx
similarity index 100%
rename from src/course/02-solutions/08-Provider/final.tsx
rename to src/course/02-lessons/02-Silver/Provider/final/final.tsx
diff --git a/src/course/02- lessons/08-Provider/lesson.mdx b/src/course/02-lessons/02-Silver/Provider/lesson.mdx
similarity index 72%
rename from src/course/02- lessons/08-Provider/lesson.mdx
rename to src/course/02-lessons/02-Silver/Provider/lesson.mdx
index ffa9838..d1c5b71 100644
--- a/src/course/02- lessons/08-Provider/lesson.mdx
+++ b/src/course/02-lessons/02-Silver/Provider/lesson.mdx
@@ -1,8 +1,8 @@
import { Meta } from '@storybook/blocks';
-
+
-# Provider Pattern
+# ๐ Provider Pattern
The Provider Pattern is a design approach used to efficiently supply data to various parts of an application without the need to manually pass it down through multiple layers of components. In the context of React, this pattern helps avoid "prop drilling," where props have to be passed through each component in the hierarchy until they reach the desired component. Instead, by utilizing React Context, you can create a provider component that wraps parts of your app, making the necessary data or functionality available to all nested components. These components can then access the data or functions directly via custom React hooks, leading to cleaner, more maintainable code.
@@ -34,6 +34,21 @@ const Page = () => {
In this lesson we are going to implement the PokemonManager with the react content pattern and pull that information from a hook in our component. Head over to the exercise.tsx file to get started.
+## When to use this pattern?
+
+**Use Provider pattern for:**
+
+- **Global State**: App-wide data like user authentication, themes
+- **Avoiding Prop Drilling**: When props pass through many components
+- **Shared Logic**: Common functionality across component trees
+- **Configuration**: App settings that many components need
+
+**Avoid when:**
+
+- **Local State**: Component-specific state should stay local
+- **Simple Apps**: Small apps don't need global state management
+- **Performance**: Context changes re-render all consumers
+
## Feedback
Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
diff --git a/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..e383510
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Silver/๐ Render Children Pattern/Exercise',
+ component: Exercise,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
\ No newline at end of file
diff --git a/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx
new file mode 100644
index 0000000..ab300d3
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/RenderChildren/exercise/exercise.tsx
@@ -0,0 +1,174 @@
+import { useState, useEffect } from 'react';
+
+interface IPokemon {
+ id: number;
+ name: string;
+ type: string;
+ hp: number;
+}
+
+/*
+ * Observations
+ * ๐ Search component has hardcoded display for all states
+ * Loading, success, and error displays are tightly coupled
+
+ * Tasks
+ * 1A ๐ป - Add render props to IPokemonSearchProps:
+ * renderLoading: () => React.ReactNode;
+ * renderSuccess: (pokemon: IPokemon[]) => React.ReactNode;
+ * renderError: (error: string) => React.ReactNode;
+ *
+ * 1B ๐ป - Replace hardcoded JSX with render function calls
+ * 1C ๐ป - In Exercise component, provide render functions for each state
+*/
+
+interface IPokemonSearchProps {
+ searchTerm: string;
+}
+
+const mockPokemon: IPokemon[] = [
+ { id: 1, name: 'Pikachu', type: 'Electric', hp: 35 },
+ { id: 4, name: 'Charmander', type: 'Fire', hp: 39 },
+ { id: 7, name: 'Squirtle', type: 'Water', hp: 44 },
+ { id: 25, name: 'Pichu', type: 'Electric', hp: 20 }
+];
+
+export const PokemonSearch = ({
+ searchTerm
+}: IPokemonSearchProps) => {
+ const [loading, setLoading] = useState(false);
+ const [pokemon, setPokemon] = useState([]);
+ const [error, setError] = useState(null);
+
+ // Simulate API call
+ const searchPokemon = async (term: string) => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ if (term === 'error') {
+ throw new Error('Pokemon not found!');
+ }
+
+ const results = mockPokemon.filter((p) =>
+ p.name.toLowerCase().includes(term.toLowerCase())
+ );
+ setPokemon(results);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Auto-search when searchTerm changes
+ useEffect(() => {
+ if (searchTerm) {
+ searchPokemon(searchTerm);
+ }
+ }, [searchTerm]);
+
+ if (loading) {
+ return (
+
+ );
+};
diff --git a/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx b/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx
new file mode 100644
index 0000000..59ca77b
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/RenderChildren/lesson.mdx
@@ -0,0 +1,47 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ Render Children Pattern
+
+Render Children is a pattern where you pass a render function through a custom prop (not children). This gives you explicit control over the API and allows multiple render functions.
+
+## Basic Example
+
+```jsx
+const PokemonLoader = ({ renderPokemon, renderLoading }) => {
+ const [pokemon, loading] = usePokemon();
+
+ if (loading) return renderLoading();
+ return renderPokemon(pokemon);
+};
+
+// Usage
+
{pokemon.name}
}
+ renderLoading={() =>
Loading Pokemon...
}
+/>;
+```
+
+## Exercise
+
+In this exercise we have a Pokemon search component that handles loading, success, and error states but only supports one fixed display format. Different teams need different ways to display search results.
+
+Your task is to refactor the search component to use render children pattern with separate render functions for each state.
+
+Head over to the exercise file and let's begin.
+
+## Why use this pattern?
+
+Render Children is valuable for:
+
+- **Multiple Render Functions**: Support different render functions for different states
+- **Explicit API**: Clear, named props instead of overloaded children
+- **Conditional Rendering**: Easy to handle multiple UI states
+- **Library Design**: Common in data fetching and state management libraries
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/06-Controlled/exercise.stories.tsx b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx
similarity index 86%
rename from src/course/02- lessons/06-Controlled/exercise.stories.tsx
rename to src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx
index 57b9197..ee6894a 100644
--- a/src/course/02- lessons/06-Controlled/exercise.stories.tsx
+++ b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/06 - Controlled Components Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Silver/๐จ Render Props Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx
new file mode 100644
index 0000000..4485f12
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/RenderProps/exercise/exercise.tsx
@@ -0,0 +1,106 @@
+interface ITypeEffectiveness {
+ attacking: string;
+ defending: string;
+ effectiveness: number;
+ description: string;
+}
+
+/*
+ * Observations
+ * ๐ Type effectiveness calculator is tightly coupled with table display
+ * Logic and presentation are mixed together
+
+ * Tasks
+ * 1A ๐ป - Add render prop to IPokemonTypeCalculatorProps:
+ * render: (effectiveness: ITypeEffectiveness[]) => React.ReactNode;
+ *
+ * 1B ๐ป - Replace the hardcoded table JSX with render prop call
+ * 1C ๐ป - In Exercise component, use render prop to display results
+*/
+
+interface IPokemonTypeCalculatorProps {
+ attackingType: string;
+}
+
+const typeChart: Record> = {
+ Fire: { Grass: 2, Water: 0.5, Fire: 0.5, Electric: 1, Ice: 2 },
+ Water: { Fire: 2, Grass: 0.5, Water: 0.5, Electric: 1, Ice: 1 },
+ Grass: { Water: 2, Fire: 0.5, Grass: 0.5, Electric: 1, Ice: 1 },
+ Electric: { Water: 2, Fire: 1, Grass: 0.5, Electric: 0.5, Ice: 1 },
+ Ice: { Grass: 2, Fire: 0.5, Water: 0.5, Electric: 1, Ice: 0.5 }
+};
+
+const getEffectivenessDescription = (value: number): string => {
+ if (value === 2) return 'Super Effective';
+ if (value === 0.5) return 'Not Very Effective';
+ return 'Normal Damage';
+};
+
+export const PokemonTypeCalculator = ({
+ attackingType
+}: IPokemonTypeCalculatorProps) => {
+ const defendingTypes = Object.keys(typeChart);
+
+ const effectiveness: ITypeEffectiveness[] = defendingTypes.map(
+ (defendingType) => ({
+ attacking: attackingType,
+ defending: defendingType,
+ effectiveness: typeChart[attackingType]?.[defendingType] ?? 1,
+ description: getEffectivenessDescription(
+ typeChart[attackingType]?.[defendingType] ?? 1
+ )
+ })
+ );
+
+ return (
+
+ );
+};
diff --git a/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx
new file mode 100644
index 0000000..9867a3f
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/RenderProps/lesson.mdx
@@ -0,0 +1,48 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐จ Render Props Pattern
+
+Render props is a pattern where you pass a function as a prop (typically called **render**) that returns a React element. The component calls this function with data, allowing complete control over rendering.
+
+## Basic Example
+
+```jsx
+const PokemonStats = ({ render }) => {
+ const stats = { hp: 340, attack: 284 };
+ return render(stats);
+};
+
+// Usage
+ (
+
+ HP: {hp}, Attack: {attack}
+
+ )}
+/>;
+```
+
+## Exercise
+
+In this exercise we have a Pokemon type effectiveness calculator that's tightly coupled with a specific display format. Different teams want to use the calculation logic but with their own display components.
+
+Your task is to refactor the component to use the render props pattern with a **render** prop, separating the calculation logic from the display.
+
+Head over to the exercise file and let's begin.
+
+## Why use this pattern?
+
+Render props is valuable for:
+
+- **Explicit API**: Clear, named render prop instead of overloaded children
+- **Reusable Logic**: Share complex calculations across different UI implementations
+- **Library Design**: Common pattern in popular libraries like React Router and Formik
+- **Flexible Rendering**: Complete control over how data is displayed
+
+## Feedback
+
+Feedback is a gift and it helps me make this course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/05-Hooks/exercise.stories.tsx b/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx
similarity index 86%
rename from src/course/02- lessons/05-Hooks/exercise.stories.tsx
rename to src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx
index 28c98ec..3880071 100644
--- a/src/course/02- lessons/05-Hooks/exercise.stories.tsx
+++ b/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/05 - Hooks Pattern/02-Exercise',
+ title: 'Lessons/๐ฅ Silver/โ๏ธ State Reducer Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02- lessons/09-StateReducer/exercise.tsx b/src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.tsx
similarity index 100%
rename from src/course/02- lessons/09-StateReducer/exercise.tsx
rename to src/course/02-lessons/02-Silver/StateReducer/exercise/exercise.tsx
diff --git a/src/course/02-solutions/06-Controlled/final.stories.tsx b/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx
similarity index 86%
rename from src/course/02-solutions/06-Controlled/final.stories.tsx
rename to src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx
index 830039c..7c4c75c 100644
--- a/src/course/02-solutions/06-Controlled/final.stories.tsx
+++ b/src/course/02-lessons/02-Silver/StateReducer/final/final.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Final } from './final';
const meta: Meta = {
- title: 'Lessons/06 - Controlled Components Pattern/03-Final',
+ title: 'Lessons/๐ฅ Silver/โ๏ธ State Reducer Pattern/03-Final',
component: Final
};
diff --git a/src/course/02-solutions/09-StateReducer/final.tsx b/src/course/02-lessons/02-Silver/StateReducer/final/final.tsx
similarity index 93%
rename from src/course/02-solutions/09-StateReducer/final.tsx
rename to src/course/02-lessons/02-Silver/StateReducer/final/final.tsx
index 7414940..2f12d90 100644
--- a/src/course/02-solutions/09-StateReducer/final.tsx
+++ b/src/course/02-lessons/02-Silver/StateReducer/final/final.tsx
@@ -2,7 +2,7 @@ import { Dispatch, useEffect, useReducer } from 'react';
import {
IPokemon,
PokemonManager
-} from '../../../shared/modules/PokemonManager/PokemonManager';
+} from '@shared/modules/PokemonManager/PokemonManager';
interface IPokemonReducerState {
pokemons?: IPokemon[];
@@ -27,10 +27,10 @@ const fetchPokemons = async (dispatch: Dispatch) => {
try {
await pokemonManager.fetchPokemons(12);
- const pokemoneState = pokemonManager.getState();
+ const pokemonState = pokemonManager.getState();
dispatch({
type: ActionNames.RECEIVED_POKEMONS,
- payload: pokemoneState.pokemons
+ payload: pokemonState.pokemons
});
} catch (e) {
dispatch({ type: ActionNames.ERROR_POKEMONS });
@@ -115,8 +115,7 @@ export const Final = () => {
+ );
+};
diff --git a/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx b/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx
new file mode 100644
index 0000000..72362cc
--- /dev/null
+++ b/src/course/02-lessons/02-Silver/Uncontrolled/lesson.mdx
@@ -0,0 +1,70 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ฎ Uncontrolled Components
+
+Uncontrolled components manage their own state internally using refs instead of React state. The DOM itself becomes the "source of truth" for the form data, making them useful for simple forms or when integrating with non-React code.
+
+## Basic Example
+
+```jsx
+// Using refs
+const PokemonNameForm = () => {
+ const nameRef = useRef();
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ console.log('Pokemon name:', nameRef.current.value);
+ };
+
+ return (
+
+ );
+};
+
+// Using FormData (modern approach)
+const PokemonNameForm = () => {
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ const formData = new FormData(e.target);
+ console.log('Pokemon name:', formData.get('pokemonName'));
+ };
+
+ return (
+
+ );
+};
+```
+
+## Exercise
+
+In this exercise we have a Pokemon team registration form that uses controlled components with lots of state management. For simple forms like this, uncontrolled components can reduce complexity and boilerplate code.
+
+Your task is to refactor the form to use uncontrolled components with refs, removing the need for state management while maintaining the same functionality.
+
+Head over to the exercise file and let's begin.
+
+## When to use this pattern?
+
+**Use uncontrolled components for:**
+- **Simple Forms**: Basic forms without complex validation
+- **Performance**: When you want to avoid re-renders on input changes
+- **Third-party Integration**: Easier integration with non-React libraries
+- **Default Values**: When you just need initial form values
+
+**Avoid uncontrolled components when:**
+- **Real-time Validation**: You need to validate as users type
+- **Dynamic UI**: Form fields depend on other field values
+- **Complex UX**: Conditional enabling/disabling of form elements
+- **Immediate Feedback**: You need to show live character counts, formatting, etc.
+
+## Feedback
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
\ No newline at end of file
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.
+
+
+ );
+};
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..c2c391a
--- /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 Pattern/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 (
+
+
+
+
+ );
+};
+
+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..816603b
--- /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/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..d381ce3
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Gold/๐ญ Headless Components/Exercise',
+ component: Exercise,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
\ No newline at end of file
diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx
new file mode 100644
index 0000000..f69e8dd
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/HeadlessComponents/exercise/exercise.tsx
@@ -0,0 +1,162 @@
+import { useState } from 'react';
+
+interface IPokemon {
+ id: number;
+ name: string;
+ type: string;
+ level: number;
+ caught: boolean;
+}
+
+/*
+ * Observations
+ * ๐ Pokemon inventory logic is tightly coupled with card UI
+ * Filtering, sorting, and state management mixed with presentation
+ * Hard to reuse logic for different UI designs
+
+ * Tasks
+ * 1A ๐ป - Extract inventory logic into usePokemonInventory hook
+ * 1B ๐ป - Return state and actions from the headless hook
+ * 1C ๐ป - Create CardView component that uses the headless logic
+ * 1D ๐ป - Create ListView component with same logic, different UI
+ * 1E ๐ป - Test both components work with shared functionality
+*/
+
+const mockPokemon: IPokemon[] = [
+ {
+ id: 1,
+ name: 'Pikachu',
+ type: 'Electric',
+ level: 25,
+ caught: true
+ },
+ {
+ id: 4,
+ name: 'Charmander',
+ type: 'Fire',
+ level: 12,
+ caught: false
+ },
+ { id: 7, name: 'Squirtle', type: 'Water', level: 18, caught: true },
+ {
+ id: 25,
+ name: 'Pichu',
+ type: 'Electric',
+ level: 8,
+ caught: false
+ },
+ {
+ id: 150,
+ name: 'Mewtwo',
+ type: 'Psychic',
+ level: 70,
+ caught: true
+ }
+];
+
+export const PokemonInventory = () => {
+ const [pokemon, setPokemon] = useState(mockPokemon);
+ const [filter, setFilter] = useState('all');
+ const [sortBy, setSortBy] = useState('name');
+
+ const toggleCaught = (id: number) => {
+ setPokemon((prev) =>
+ prev.map((p) => (p.id === id ? { ...p, caught: !p.caught } : p))
+ );
+ };
+
+ const filteredPokemon = pokemon.filter((p) => {
+ if (filter === 'caught') return p.caught;
+ if (filter === 'wild') return !p.caught;
+ return true;
+ });
+
+ const sortedPokemon = [...filteredPokemon].sort((a, b) => {
+ if (sortBy === 'level') return b.level - a.level;
+ if (sortBy === 'type') return a.type.localeCompare(b.type);
+ return a.name.localeCompare(b.name);
+ });
+
+ return (
+
+
Pokemon Inventory
+
+ {/* Controls */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Pokemon Cards */}
+
+ {sortedPokemon.map((p) => (
+
+
+
{p.name}
+
+ {p.caught ? 'Caught' : 'Wild'}
+
+
+
+
+
Type: {p.type}
+
Level: {p.level}
+
+
+
+
+ ))}
+
+
+
+ Showing {sortedPokemon.length} of {pokemon.length} Pokemon
+
+ );
+};
diff --git a/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx b/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx
new file mode 100644
index 0000000..508777f
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/HeadlessComponents/lesson.mdx
@@ -0,0 +1,61 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# ๐ญ Headless Components
+
+Headless components provide logic and behavior without any UI. They separate business logic from presentation, allowing complete control over styling while reusing complex functionality across different designs.
+
+## Basic Example
+
+```jsx
+// Headless component - logic only
+const usePokemonBattle = () => {
+ const [hp, setHp] = useState(100);
+ const attack = () => setHp((prev) => Math.max(0, prev - 20));
+ const heal = () => setHp((prev) => Math.min(100, prev + 30));
+
+ return { hp, attack, heal, isDefeated: hp === 0 };
+};
+
+// UI components use the headless logic
+const BattleCard = () => {
+ const { hp, attack, heal, isDefeated } = usePokemonBattle();
+ return (
+
+
HP: {hp}
+
+
+
+ );
+};
+```
+
+## Exercise
+
+In this exercise we have a Pokemon inventory system with tightly coupled UI and logic. Different teams need the same inventory functionality but with completely different designs - mobile cards, desktop tables, and admin dashboards.
+
+Your task is to extract the inventory logic into a headless component, then create multiple UI implementations that use the same underlying functionality.
+
+Head over to the exercise file and let's begin.
+
+## When to use this pattern?
+
+**Use headless components for:**
+
+- **Complex Logic**: State machines, data fetching, form validation
+- **Multiple UIs**: Same logic needed across different designs
+- **Design Systems**: Consistent behavior with flexible presentation
+- **Testing**: Easier to test logic separately from UI
+
+**Avoid when:**
+
+- **Simple Components**: Basic UI with minimal logic
+- **Single Use**: Logic only needed in one place
+- **Tight Coupling**: UI and logic are inherently connected
+
+## Feedback
+
+Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx
similarity index 85%
rename from src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx
rename to src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx
index 09540c2..ec8548e 100644
--- a/src/course/02- lessons/07-HigherOrderComponents/exercise.stories.tsx
+++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.stories.tsx
@@ -3,7 +3,8 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Exercise } from './exercise';
const meta: Meta = {
- title: 'Lessons/07 - Higher Order Components Pattern/02-Exercise',
+ title:
+ 'Lessons/๐ฅ Gold/๐ Higher Order Components Pattern/02-Exercise',
component: Exercise
};
diff --git a/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx
new file mode 100644
index 0000000..20ae3ee
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/HigherOrderComponents/exercise/exercise.tsx
@@ -0,0 +1,77 @@
+// No imports needed for this exercise
+
+interface IPokemon {
+ id: number;
+ name: string;
+ type: string;
+ level: number;
+}
+
+/*
+ * Observations
+ * ๐ Type-based styling logic is repeated inline
+ * Hard to reuse styling across different components
+ * Mixing presentation logic with component logic
+
+ * Tasks
+ * 1A ๐ป - Create withPokemonType HOC that adds type-based styling
+ * 1B ๐ป - Apply HOC to PokemonCard component
+ * 1C ๐ป - Use enhanced component to display Pokemon with type styling
+ * 1D ๐ป - Test that different Pokemon types get different styling
+*/
+
+// Sample Pokemon data
+const samplePokemon: IPokemon[] = [
+ { id: 1, name: 'Pikachu', type: 'Electric', level: 25 },
+ { id: 4, name: 'Charmander', type: 'Fire', level: 12 },
+ { id: 7, name: 'Squirtle', type: 'Water', level: 18 }
+];
+
+// Basic Pokemon Card Component
+const PokemonCard = ({ pokemon }: { pokemon: IPokemon }) => (
+
;
+const StyledPokemonCard = withPokemonType(PokemonCard);
+
+// Renders with type-specific styling
+;
+```
+
+## Exercise
+
+In this exercise we have Pokemon components that need common functionality like loading states, error handling, and type-based styling. Instead of duplicating this logic, we'll create HOCs to wrap components with these features.
+
+Your task is to create HOCs that add loading states, error boundaries, and Pokemon type styling to any component.
+
+Head over to the exercise file and let's begin.
+
+## When to use this pattern?
+
+**Use HOCs for:**
+
+- **Cross-cutting Concerns**: Authentication, logging, analytics
+- **Code Reuse**: Same functionality across multiple components
+- **Legacy Support**: Wrapping class components with modern features
+- **Third-party Integration**: Adding external library features
+
+**Avoid when:**
+
+- **Simple Logic**: Use hooks for simpler state management
+- **Modern React**: Prefer hooks and composition over HOCs
+- **Complex Props**: HOCs can make prop flow confusing
+
+## Feedback
+
+Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx
new file mode 100644
index 0000000..e0cc250
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Exercise } from './exercise';
+
+const meta: Meta = {
+ title: 'Lessons/๐ฅ Gold/โณ Suspense & Lazy Loading/Exercise',
+ component: Exercise,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
\ No newline at end of file
diff --git a/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx
new file mode 100644
index 0000000..f3bbdb4
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/Suspense/exercise/exercise.tsx
@@ -0,0 +1,185 @@
+import { useState } from 'react';
+// Utilities provided for you:
+// import { delay } from '../utils/delay';
+// import { PokemonLoader } from '../utils/PokemonLoader';
+
+/*
+ * Observations
+ * ๐ All Pokemon components are imported upfront
+ * Large initial bundle size even if user doesn't view all Pokemon
+ * No loading states for heavy components
+
+ * Available utilities (already provided):
+ * - import { delay } from '../utils/delay';
+ * - import { PokemonLoader } from '../utils/PokemonLoader';
+
+ * Tasks
+ * 1A ๐จ๐ป๐ป - Move Pokemon components to separate files with default exports
+ * 1B ๐จ๐ป๐ป - Add use(delay(ms)) to each component with cached promises
+ * 1C ๐จ๐ป๐ป - Convert imports to React.lazy() dynamic imports
+ * 1D ๐จ๐ป๐ป - Wrap Pokemon components with Suspense using PokemonLoader
+ * 1E ๐จ๐ป๐ป - Test that components show loading states and resolve correctly
+*/
+
+// Heavy Pokemon detail components (simulating large components)
+const PikachuDetails = () => (
+
+
+ Pikachu
+
+
+
+
Stats
+
HP: 35
+
Attack: 55
+
Defense: 40
+
Speed: 90
+
+
+
Abilities
+
Static
+
Lightning Rod (Hidden)
+
+
+
+
Description
+
+ This Pokemon has electricity-storing pouches on its cheeks.
+ These appear to become electrically charged during the night
+ while Pikachu sleeps.
+
+
+
+);
+
+const CharizardDetails = () => (
+
+
+ Charizard
+
+
+
+
Stats
+
HP: 78
+
Attack: 84
+
Defense: 78
+
Speed: 100
+
+
+
Abilities
+
Blaze
+
Solar Power (Hidden)
+
+
+
+
Description
+
+ Charizard flies around the sky in search of powerful
+ opponents. It breathes fire of such great heat that it melts
+ anything.
+
+
+
+);
+
+const BlastoiseDetails = () => (
+
+
+ Blastoise
+
+
+
+
Stats
+
HP: 79
+
Attack: 83
+
Defense: 100
+
Speed: 78
+
+
+
Abilities
+
Torrent
+
Rain Dish (Hidden)
+
+
+
+
Description
+
+ Blastoise has water spouts that protrude from its shell. The
+ water spouts are very accurate and can punch through thick
+ steel.
+
+ );
+};
diff --git a/src/course/02-lessons/03-Gold/Suspense/lesson.mdx b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx
new file mode 100644
index 0000000..40b6849
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/Suspense/lesson.mdx
@@ -0,0 +1,51 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# โณ Suspense & Lazy Loading
+
+Suspense allows you to declaratively handle loading states while React.lazy enables code splitting by dynamically importing components only when needed. This improves initial bundle size and loading performance.
+
+## Basic Example
+
+```jsx
+import { Suspense, lazy } from 'react';
+
+// Lazy load component
+const PokemonDetails = lazy(() => import('./PokemonDetails'));
+
+const App = () => (
+ Loading Pokemon...
}>
+
+
+);
+```
+
+## Exercise
+
+In this exercise we have a Pokemon encyclopedia app that imports all Pokemon detail components upfront, creating a large initial bundle. Users might only view a few Pokemon, making this inefficient.
+
+Your task is to implement lazy loading with Suspense to split the code and only load Pokemon components when they're actually needed.
+
+Head over to the exercise file and let's begin.
+
+## When to use this pattern?
+
+**Use Suspense & Lazy Loading for:**
+
+- **Large Components**: Heavy components that aren't immediately needed
+- **Route-based Splitting**: Different pages/routes in your app
+- **Conditional Features**: Components shown based on user actions
+- **Performance**: Reducing initial bundle size
+
+**Avoid when:**
+
+- **Small Components**: Overhead isn't worth it for tiny components
+- **Critical Path**: Components needed immediately on page load
+- **Frequent Toggling**: Components that show/hide rapidly
+
+## Feedback
+
+Feedback is a gift and it helps me make these course better for you. If you have a spare 5 mins please could you fill out a feedback form for me. Thank you.
+
+[Feedback](https://github.com/code-mattclaffey/react-design-patterns/issues/new)
diff --git a/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx b/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx
new file mode 100644
index 0000000..82d3580
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/Suspense/utils/PokemonLoader.tsx
@@ -0,0 +1,23 @@
+// Loading fallback component for Pokemon data
+export const PokemonLoader = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Pokemon data...
+
+);
\ No newline at end of file
diff --git a/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts b/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts
new file mode 100644
index 0000000..5d30227
--- /dev/null
+++ b/src/course/02-lessons/03-Gold/Suspense/utils/delay.ts
@@ -0,0 +1,2 @@
+// Utility function to create a delay promise
+export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
\ No newline at end of file
diff --git a/src/course/02-solutions/01-ConditionalRendering/final.stories.tsx b/src/course/02-solutions/01-ConditionalRendering/final.stories.tsx
deleted file mode 100644
index a17bb30..0000000
--- a/src/course/02-solutions/01-ConditionalRendering/final.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { userEvent, within, expect } from '@storybook/test';
-
-import { ComponentOne } from './final';
-
-const meta: Meta = {
- title: 'Lessons/01 - Conditional Rendering Pattern/03-Final',
- component: ComponentOne
-};
-
-export default meta;
-type Story = StoryObj;
-
-const username = 'John Doe';
-
-/*
- * 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 ({ canvasElement }) => {
- const canvas = within(canvasElement);
-
- await userEvent.click(canvas.getByRole('button', { name: 'Login' }));
-
- await expect(canvas.getByText(`Welcome ${username}`)).toBeInTheDocument();
- await expect(canvas.queryByRole('button', { name: 'Login' })).toBeNull();
-
- await userEvent.click(canvas.getByRole('button', { name: 'Logout' }));
-
- await expect(canvas.queryByText(`Welcome ${username}`)).toBeNull();
- await expect(canvas.queryByRole('button', { name: 'Logout' })).toBeNull();
- },
- args: {
- username
- }
-};
diff --git a/src/course/02-solutions/01-ConditionalRendering/final.tsx b/src/course/02-solutions/01-ConditionalRendering/final.tsx
deleted file mode 100644
index 0a9f76d..0000000
--- a/src/course/02-solutions/01-ConditionalRendering/final.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useState } from 'react';
-import { Button } from '../../../shared/components/Button/Button.component';
-
-interface IComponentProps {
- username: string;
-}
-
-export const ComponentOne = (props: IComponentProps) => {
- const [isAuthenticated, setIsAuthenticated] = useState(false);
-
- const onLogin = () => {
- setIsAuthenticated(true);
- };
-
- const onLogout = () => {
- setIsAuthenticated(false);
- };
-
- return (
-
- {/* Other components */}
- {!isAuthenticated && }
- {isAuthenticated && (
- <>
-
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at ante.
- Eros lacus faucibus consequat scelerisque proin volutpat. In
- pellentesque est curae; dapibus nisl risus sociosqu penatibus.
- Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi
- dis mauris magna posuere? Cum class viverra bibendum rutrum
- odio scelerisque scelerisque libero, nisl est convallis non.
- Ac convallis odio suspendisse velit mollis libero. Morbi enim
- blandit venenatis lorem!
-
-
-
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at ante.
- Eros lacus faucibus consequat scelerisque proin volutpat. In
- pellentesque est curae; dapibus nisl risus sociosqu penatibus.
- Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi
- dis mauris magna posuere? Cum class viverra bibendum rutrum
- odio scelerisque scelerisque libero, nisl est convallis non.
- Ac convallis odio suspendisse velit mollis libero. Morbi enim
- blandit venenatis lorem!
-
-
-
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at ante.
- Eros lacus faucibus consequat scelerisque proin volutpat. In
- pellentesque est curae; dapibus nisl risus sociosqu penatibus.
- Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi
- dis mauris magna posuere? Cum class viverra bibendum rutrum
- odio scelerisque scelerisque libero, nisl est convallis non.
- Ac convallis odio suspendisse velit mollis libero. Morbi enim
- blandit venenatis lorem!
-
-
-
-
- Per torquent, mus cursus hendrerit id aenean justo auctor
- donec. Turpis magna et, egestas dignissim nascetur. Sapien
- augue nisl varius diam aliquet. Litora velit, tortor at ante.
- Eros lacus faucibus consequat scelerisque proin volutpat. In
- pellentesque est curae; dapibus nisl risus sociosqu penatibus.
- Lobortis pulvinar scelerisque lacus. Elit vel eros facilisi
- dis mauris magna posuere? Cum class viverra bibendum rutrum
- odio scelerisque scelerisque libero, nisl est convallis non.
- Ac convallis odio suspendisse velit mollis libero. Morbi enim
- blandit venenatis lorem!
-
-
-
-);
diff --git a/src/course/02-solutions/11-Slots/final.stories.tsx b/src/course/02-solutions/11-Slots/final.stories.tsx
deleted file mode 100644
index dc71714..0000000
--- a/src/course/02-solutions/11-Slots/final.stories.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { Final } from './final';
-
-const meta: Meta = {
- title: 'Lessons/11 - Slots/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-solutions/11-Slots/final.tsx b/src/course/02-solutions/11-Slots/final.tsx
deleted file mode 100644
index fb91eb8..0000000
--- a/src/course/02-solutions/11-Slots/final.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import classNames from 'classnames';
-import { HTMLAttributes } from 'react';
-import { IconOne, IconTwo } from './icons';
-
-interface IButton extends HTMLAttributes {
- className?: string;
- iconLeft?: React.ReactNode;
- iconRight?: React.ReactNode;
- children: React.ReactNode | React.ReactNode[];
-}
-
-const buttonClasses =
- 'middle none center rounded-lg bg-blue-500 py-3 px-6 font-sans text-xs font-bold uppercase text-white shadow-md shadow-blue-500/20 transition-all hover:shadow-lg hover:shadow-blue-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none inline-flex items-center justify-center';
-
-export const Button = ({
- className,
- children,
- iconLeft,
- iconRight,
- ...rest
-}: IButton) => {
- return (
-
- );
-};
-
-export const Final = () => (
-