Skip to content

Commit a6a6f76

Browse files
feat: implement suspense
1 parent 962cf31 commit a6a6f76

File tree

13 files changed

+1579
-682
lines changed

13 files changed

+1579
-682
lines changed

package-lock.json

Lines changed: 1057 additions & 677 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
"dependencies": {
1717
"@vercel/analytics": "^1.3.1",
1818
"classnames": "^2.5.1",
19-
"react": "^18.3.1",
19+
"react": "^19.0.0",
2020
"react-code-blocks": "^0.1.6",
21-
"react-dom": "^18.3.1",
21+
"react-dom": "^19.0.0",
2222
"react-focus-lock": "^2.12.1"
2323
},
2424
"devDependencies": {
@@ -35,8 +35,8 @@
3535
"@storybook/test": "^8.4.7",
3636
"@storybook/test-runner": "^0.19.0",
3737
"@storybook/theming": "^8.4.7",
38-
"@types/react": "^18.3.3",
39-
"@types/react-dom": "^18.3.0",
38+
"@types/react": "^19.0.0",
39+
"@types/react-dom": "^19.0.0",
4040
"@typescript-eslint/eslint-plugin": "^7.13.1",
4141
"@typescript-eslint/parser": "^7.13.1",
4242
"@vitejs/plugin-react": "^4.3.1",
@@ -52,6 +52,6 @@
5252
"storybook": "^8.4.7",
5353
"tailwindcss": "^3.4.5",
5454
"typescript": "^5.2.2",
55-
"vite": "^5.3.1"
55+
"vite": "^7.1.4"
5656
}
5757
}

src/course/01-introduction/01-Welcome.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi
6262
#### 🥇 Gold
6363

6464
- [Higher order component](?path=/docs/lessons-🥇-gold-higher-order-components-pattern-01-lesson--docs)
65+
- [Suspense & lazy loading pattern](?path=/docs/lessons-🥇-gold-suspense-lazy-loading-01-lesson--docs)
6566

6667
## FAQs
6768

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Exercise } from './exercise';
3+
4+
const meta: Meta<typeof Exercise> = {
5+
title: 'Lessons/🥇 Gold/⏳ Suspense & Lazy Loading/Exercise',
6+
component: Exercise,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { useState } from 'react';
2+
// Utilities provided for you:
3+
// import { delay } from '../utils/delay';
4+
// import { PokemonLoader } from '../utils/PokemonLoader';
5+
6+
/*
7+
* Observations
8+
* 💅 All Pokemon components are imported upfront
9+
* Large initial bundle size even if user doesn't view all Pokemon
10+
* No loading states for heavy components
11+
12+
* Available utilities (already provided):
13+
* - import { delay } from '../utils/delay';
14+
* - import { PokemonLoader } from '../utils/PokemonLoader';
15+
16+
* Tasks
17+
* 1A 👨🏻💻 - Move Pokemon components to separate files with default exports
18+
* 1B 👨🏻💻 - Add use(delay(ms)) to each component with cached promises
19+
* 1C 👨🏻💻 - Convert imports to React.lazy() dynamic imports
20+
* 1D 👨🏻💻 - Wrap Pokemon components with Suspense using PokemonLoader
21+
* 1E 👨🏻💻 - Test that components show loading states and resolve correctly
22+
*/
23+
24+
// Heavy Pokemon detail components (simulating large components)
25+
const PikachuDetails = () => (
26+
<div className="bg-yellow-50 p-6 rounded-lg">
27+
<h2 className="text-2xl font-bold text-yellow-800 mb-4">
28+
Pikachu
29+
</h2>
30+
<div className="grid grid-cols-2 gap-4">
31+
<div>
32+
<h3 className="font-bold mb-2">Stats</h3>
33+
<p>HP: 35</p>
34+
<p>Attack: 55</p>
35+
<p>Defense: 40</p>
36+
<p>Speed: 90</p>
37+
</div>
38+
<div>
39+
<h3 className="font-bold mb-2">Abilities</h3>
40+
<p>Static</p>
41+
<p>Lightning Rod (Hidden)</p>
42+
</div>
43+
</div>
44+
<div className="mt-4">
45+
<h3 className="font-bold mb-2">Description</h3>
46+
<p>
47+
This Pokemon has electricity-storing pouches on its cheeks.
48+
These appear to become electrically charged during the night
49+
while Pikachu sleeps.
50+
</p>
51+
</div>
52+
</div>
53+
);
54+
55+
const CharizardDetails = () => (
56+
<div className="bg-red-50 p-6 rounded-lg">
57+
<h2 className="text-2xl font-bold text-red-800 mb-4">
58+
Charizard
59+
</h2>
60+
<div className="grid grid-cols-2 gap-4">
61+
<div>
62+
<h3 className="font-bold mb-2">Stats</h3>
63+
<p>HP: 78</p>
64+
<p>Attack: 84</p>
65+
<p>Defense: 78</p>
66+
<p>Speed: 100</p>
67+
</div>
68+
<div>
69+
<h3 className="font-bold mb-2">Abilities</h3>
70+
<p>Blaze</p>
71+
<p>Solar Power (Hidden)</p>
72+
</div>
73+
</div>
74+
<div className="mt-4">
75+
<h3 className="font-bold mb-2">Description</h3>
76+
<p>
77+
Charizard flies around the sky in search of powerful
78+
opponents. It breathes fire of such great heat that it melts
79+
anything.
80+
</p>
81+
</div>
82+
</div>
83+
);
84+
85+
const BlastoiseDetails = () => (
86+
<div className="bg-blue-50 p-6 rounded-lg">
87+
<h2 className="text-2xl font-bold text-blue-800 mb-4">
88+
Blastoise
89+
</h2>
90+
<div className="grid grid-cols-2 gap-4">
91+
<div>
92+
<h3 className="font-bold mb-2">Stats</h3>
93+
<p>HP: 79</p>
94+
<p>Attack: 83</p>
95+
<p>Defense: 100</p>
96+
<p>Speed: 78</p>
97+
</div>
98+
<div>
99+
<h3 className="font-bold mb-2">Abilities</h3>
100+
<p>Torrent</p>
101+
<p>Rain Dish (Hidden)</p>
102+
</div>
103+
</div>
104+
<div className="mt-4">
105+
<h3 className="font-bold mb-2">Description</h3>
106+
<p>
107+
Blastoise has water spouts that protrude from its shell. The
108+
water spouts are very accurate and can punch through thick
109+
steel.
110+
</p>
111+
</div>
112+
</div>
113+
);
114+
115+
export const PokemonEncyclopedia = () => {
116+
const [selectedPokemon, setSelectedPokemon] = useState<
117+
string | null
118+
>(null);
119+
120+
const renderPokemonDetails = () => {
121+
switch (selectedPokemon) {
122+
case 'pikachu':
123+
return <PikachuDetails />;
124+
case 'charizard':
125+
return <CharizardDetails />;
126+
case 'blastoise':
127+
return <BlastoiseDetails />;
128+
default:
129+
return (
130+
<div className="bg-gray-50 p-6 rounded-lg text-center">
131+
<p className="text-gray-600">
132+
Select a Pokemon to view details
133+
</p>
134+
</div>
135+
);
136+
}
137+
};
138+
139+
return (
140+
<div className="max-w-4xl space-y-6">
141+
<h1 className="text-3xl font-bold text-center">
142+
Pokemon Encyclopedia
143+
</h1>
144+
145+
<div className="flex gap-4 justify-center">
146+
<button
147+
onClick={() => setSelectedPokemon('pikachu')}
148+
className={`px-4 py-2 rounded ${
149+
selectedPokemon === 'pikachu'
150+
? 'bg-yellow-500 text-white'
151+
: 'bg-gray-200 text-gray-700'
152+
}`}
153+
>
154+
Pikachu
155+
</button>
156+
<button
157+
onClick={() => setSelectedPokemon('charizard')}
158+
className={`px-4 py-2 rounded ${
159+
selectedPokemon === 'charizard'
160+
? 'bg-red-500 text-white'
161+
: 'bg-gray-200 text-gray-700'
162+
}`}
163+
>
164+
Charizard
165+
</button>
166+
<button
167+
onClick={() => setSelectedPokemon('blastoise')}
168+
className={`px-4 py-2 rounded ${
169+
selectedPokemon === 'blastoise'
170+
? 'bg-blue-500 text-white'
171+
: 'bg-gray-200 text-gray-700'
172+
}`}
173+
>
174+
Blastoise
175+
</button>
176+
</div>
177+
178+
{renderPokemonDetails()}
179+
</div>
180+
);
181+
};
182+
183+
export const Exercise = () => {
184+
return <PokemonEncyclopedia />;
185+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { use } from 'react';
2+
import { delay } from '../../utils/delay';
3+
4+
// Cache the promise to prevent infinite re-creation
5+
const blastoiseDataPromise = delay(1000);
6+
7+
const BlastoiseDetails = () => {
8+
use(blastoiseDataPromise);
9+
10+
return (
11+
<div className="bg-blue-50 p-6 rounded-lg">
12+
<h2 className="text-2xl font-bold text-blue-800 mb-4">
13+
Blastoise
14+
</h2>
15+
<div className="grid grid-cols-2 gap-4">
16+
<div>
17+
<h3 className="font-bold mb-2">Stats</h3>
18+
<p>HP: 79</p>
19+
<p>Attack: 83</p>
20+
<p>Defense: 100</p>
21+
<p>Speed: 78</p>
22+
</div>
23+
<div>
24+
<h3 className="font-bold mb-2">Abilities</h3>
25+
<p>Torrent</p>
26+
<p>Rain Dish (Hidden)</p>
27+
</div>
28+
</div>
29+
<div className="mt-4">
30+
<h3 className="font-bold mb-2">Description</h3>
31+
<p>
32+
Blastoise has water spouts that protrude from its shell. The
33+
water spouts are very accurate and can punch through thick
34+
steel.
35+
</p>
36+
</div>
37+
</div>
38+
);
39+
};
40+
41+
export default BlastoiseDetails;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { use } from 'react';
2+
import { delay } from '../../utils/delay';
3+
4+
// Cache the promise to prevent infinite re-creation
5+
const charizardDataPromise = delay(1800);
6+
7+
const CharizardDetails = () => {
8+
use(charizardDataPromise);
9+
10+
return (
11+
<div className="bg-red-50 p-6 rounded-lg">
12+
<h2 className="text-2xl font-bold text-red-800 mb-4">
13+
Charizard
14+
</h2>
15+
<div className="grid grid-cols-2 gap-4">
16+
<div>
17+
<h3 className="font-bold mb-2">Stats</h3>
18+
<p>HP: 78</p>
19+
<p>Attack: 84</p>
20+
<p>Defense: 78</p>
21+
<p>Speed: 100</p>
22+
</div>
23+
<div>
24+
<h3 className="font-bold mb-2">Abilities</h3>
25+
<p>Blaze</p>
26+
<p>Solar Power (Hidden)</p>
27+
</div>
28+
</div>
29+
<div className="mt-4">
30+
<h3 className="font-bold mb-2">Description</h3>
31+
<p>
32+
Charizard flies around the sky in search of powerful
33+
opponents. It breathes fire of such great heat that it melts
34+
anything.
35+
</p>
36+
</div>
37+
</div>
38+
);
39+
};
40+
41+
export default CharizardDetails;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { use } from 'react';
2+
import { delay } from '../../utils/delay';
3+
4+
// Cache the promise to prevent infinite re-creation
5+
const pikachuDataPromise = delay(1200);
6+
7+
const PikachuDetails = () => {
8+
use(pikachuDataPromise);
9+
10+
return (
11+
<div className="bg-yellow-50 p-6 rounded-lg">
12+
<h2 className="text-2xl font-bold text-yellow-800 mb-4">
13+
Pikachu
14+
</h2>
15+
<div className="grid grid-cols-2 gap-4">
16+
<div>
17+
<h3 className="font-bold mb-2">Stats</h3>
18+
<p>HP: 35</p>
19+
<p>Attack: 55</p>
20+
<p>Defense: 40</p>
21+
<p>Speed: 90</p>
22+
</div>
23+
<div>
24+
<h3 className="font-bold mb-2">Abilities</h3>
25+
<p>Static</p>
26+
<p>Lightning Rod (Hidden)</p>
27+
</div>
28+
</div>
29+
<div className="mt-4">
30+
<h3 className="font-bold mb-2">Description</h3>
31+
<p>
32+
This Pokemon has electricity-storing pouches on its cheeks.
33+
These appear to become electrically charged during the night
34+
while Pikachu sleeks.
35+
</p>
36+
</div>
37+
</div>
38+
);
39+
};
40+
41+
export default PikachuDetails;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Final } from './final';
3+
4+
const meta: Meta<typeof Final> = {
5+
title: 'Lessons/🥇 Gold/⏳ Suspense & Lazy Loading/Final',
6+
component: Final,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};

0 commit comments

Comments
 (0)