Skip to content

Commit 962cf31

Browse files
feat: implement uncontrolled components
1 parent d3b3fe5 commit 962cf31

File tree

6 files changed

+419
-0
lines changed

6 files changed

+419
-0
lines changed

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

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

5151
- [Compound components pattern](?path=/docs/lessons-🥈-Silver-compound-components-pattern-01-lesson--docs)
5252
- [Controlled component pattern](?path=/docs/lessons-🥈-Silver-controlled-components-pattern-01-lesson--docs)
53+
- [Uncontrolled components pattern](?path=/docs/lessons-🥈-silver-uncontrolled-components-01-lesson--docs)
5354
- [FACC pattern](?path=/docs/lessons-🥈-silver-facc-pattern-01-lesson--docs)
5455
- [Render children pattern](?path=/docs/lessons-🥈-silver-render-children-pattern-01-lesson--docs)
5556
- [Render props pattern](?path=/docs/lessons-🥈-Silver-render-props-pattern-01-lesson--docs)
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/🥈 Silver/🎮 Uncontrolled Components/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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { useState } from 'react';
2+
3+
interface IPokemonTeam {
4+
trainerName: string;
5+
teamName: string;
6+
pokemon1: string;
7+
pokemon2: string;
8+
pokemon3: string;
9+
}
10+
11+
/*
12+
* Observations
13+
* 💅 Form uses controlled components with lots of state management
14+
* Every input change triggers a re-render
15+
* Lots of boilerplate for simple form handling
16+
17+
* Tasks
18+
* 1A 👨🏻💻 - Replace useState with useRef for each form field
19+
* 1B 👨🏻💻 - Remove onChange handlers and use defaultValue instead of value
20+
* 1C 👨🏻💻 - Update handleSubmit to read values from refs
21+
* 1D 👨🏻💻 - Remove all state-related code
22+
*/
23+
24+
export const PokemonTeamRegistration = () => {
25+
const [trainerName, setTrainerName] = useState('');
26+
const [teamName, setTeamName] = useState('');
27+
const [pokemon1, setPokemon1] = useState('');
28+
const [pokemon2, setPokemon2] = useState('');
29+
const [pokemon3, setPokemon3] = useState('');
30+
const [submittedTeam, setSubmittedTeam] = useState<IPokemonTeam | null>(null);
31+
32+
const handleSubmit = (e: React.FormEvent) => {
33+
e.preventDefault();
34+
35+
const team: IPokemonTeam = {
36+
trainerName,
37+
teamName,
38+
pokemon1,
39+
pokemon2,
40+
pokemon3
41+
};
42+
43+
setSubmittedTeam(team);
44+
45+
// Reset form
46+
setTrainerName('');
47+
setTeamName('');
48+
setPokemon1('');
49+
setPokemon2('');
50+
setPokemon3('');
51+
};
52+
53+
return (
54+
<div className="bg-blue-50 p-6 rounded-lg max-w-md">
55+
<h2 className="text-2xl font-bold mb-4">Pokemon Team Registration</h2>
56+
57+
<form onSubmit={handleSubmit} className="space-y-4">
58+
<div>
59+
<label htmlFor="trainer-name" className="block text-sm font-medium text-gray-700 mb-1">
60+
Trainer Name
61+
</label>
62+
<input
63+
id="trainer-name"
64+
type="text"
65+
value={trainerName}
66+
onChange={(e) => setTrainerName(e.target.value)}
67+
className="w-full p-2 border rounded"
68+
required
69+
/>
70+
</div>
71+
72+
<div>
73+
<label htmlFor="team-name" className="block text-sm font-medium text-gray-700 mb-1">
74+
Team Name
75+
</label>
76+
<input
77+
id="team-name"
78+
type="text"
79+
value={teamName}
80+
onChange={(e) => setTeamName(e.target.value)}
81+
className="w-full p-2 border rounded"
82+
required
83+
/>
84+
</div>
85+
86+
<div>
87+
<label htmlFor="pokemon-1" className="block text-sm font-medium text-gray-700 mb-1">
88+
Pokemon 1
89+
</label>
90+
<input
91+
id="pokemon-1"
92+
type="text"
93+
value={pokemon1}
94+
onChange={(e) => setPokemon1(e.target.value)}
95+
className="w-full p-2 border rounded"
96+
placeholder="e.g., Pikachu"
97+
required
98+
/>
99+
</div>
100+
101+
<div>
102+
<label htmlFor="pokemon-2" className="block text-sm font-medium text-gray-700 mb-1">
103+
Pokemon 2
104+
</label>
105+
<input
106+
id="pokemon-2"
107+
type="text"
108+
value={pokemon2}
109+
onChange={(e) => setPokemon2(e.target.value)}
110+
className="w-full p-2 border rounded"
111+
placeholder="e.g., Charizard"
112+
required
113+
/>
114+
</div>
115+
116+
<div>
117+
<label htmlFor="pokemon-3" className="block text-sm font-medium text-gray-700 mb-1">
118+
Pokemon 3
119+
</label>
120+
<input
121+
id="pokemon-3"
122+
type="text"
123+
value={pokemon3}
124+
onChange={(e) => setPokemon3(e.target.value)}
125+
className="w-full p-2 border rounded"
126+
placeholder="e.g., Blastoise"
127+
required
128+
/>
129+
</div>
130+
131+
<button
132+
type="submit"
133+
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
134+
>
135+
Register Team
136+
</button>
137+
</form>
138+
139+
{submittedTeam && (
140+
<div className="mt-6 p-4 bg-green-100 rounded">
141+
<h3 className="font-bold text-green-800 mb-2">Team Registered!</h3>
142+
<p><strong>Trainer:</strong> {submittedTeam.trainerName}</p>
143+
<p><strong>Team:</strong> {submittedTeam.teamName}</p>
144+
<p><strong>Pokemon:</strong> {submittedTeam.pokemon1}, {submittedTeam.pokemon2}, {submittedTeam.pokemon3}</p>
145+
</div>
146+
)}
147+
</div>
148+
);
149+
};
150+
151+
export const Exercise = () => {
152+
return <PokemonTeamRegistration />;
153+
};
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/🥈 Silver/🎮 Uncontrolled Components/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 = {};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { useState } from 'react';
2+
3+
interface IPokemonTeam {
4+
trainerName: string;
5+
teamName: string;
6+
pokemon1: string;
7+
pokemon2: string;
8+
pokemon3: string;
9+
}
10+
11+
export const PokemonTeamRegistration = () => {
12+
// Only keeping state for the submitted team display
13+
const [submittedTeam, setSubmittedTeam] =
14+
useState<IPokemonTeam | null>(null);
15+
16+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
17+
e.preventDefault();
18+
19+
// Using FormData - still uncontrolled!
20+
const formData = new FormData(e.currentTarget);
21+
const team: IPokemonTeam = {
22+
trainerName: formData.get('trainerName') as string,
23+
teamName: formData.get('teamName') as string,
24+
pokemon1: formData.get('pokemon1') as string,
25+
pokemon2: formData.get('pokemon2') as string,
26+
pokemon3: formData.get('pokemon3') as string
27+
};
28+
29+
setSubmittedTeam(team);
30+
31+
// Reset form using built-in method
32+
e.currentTarget.reset();
33+
};
34+
35+
return (
36+
<div className="bg-blue-50 p-6 rounded-lg max-w-md">
37+
<h2 className="text-2xl font-bold mb-4">
38+
Pokemon Team Registration
39+
</h2>
40+
41+
<form onSubmit={handleSubmit} className="space-y-4">
42+
<div>
43+
<label
44+
htmlFor="trainer-name"
45+
className="block text-sm font-medium text-gray-700 mb-1"
46+
>
47+
Trainer Name
48+
</label>
49+
<input
50+
id="trainer-name"
51+
name="trainerName"
52+
type="text"
53+
defaultValue=""
54+
className="w-full p-2 border rounded"
55+
required
56+
/>
57+
</div>
58+
59+
<div>
60+
<label
61+
htmlFor="team-name"
62+
className="block text-sm font-medium text-gray-700 mb-1"
63+
>
64+
Team Name
65+
</label>
66+
<input
67+
id="team-name"
68+
name="teamName"
69+
type="text"
70+
defaultValue=""
71+
className="w-full p-2 border rounded"
72+
required
73+
/>
74+
</div>
75+
76+
<div>
77+
<label
78+
htmlFor="pokemon-1"
79+
className="block text-sm font-medium text-gray-700 mb-1"
80+
>
81+
Pokemon 1
82+
</label>
83+
<input
84+
id="pokemon-1"
85+
name="pokemon1"
86+
type="text"
87+
defaultValue=""
88+
className="w-full p-2 border rounded"
89+
placeholder="e.g., Pikachu"
90+
required
91+
/>
92+
</div>
93+
94+
<div>
95+
<label
96+
htmlFor="pokemon-2"
97+
className="block text-sm font-medium text-gray-700 mb-1"
98+
>
99+
Pokemon 2
100+
</label>
101+
<input
102+
id="pokemon-2"
103+
name="pokemon2"
104+
type="text"
105+
defaultValue=""
106+
className="w-full p-2 border rounded"
107+
placeholder="e.g., Charizard"
108+
required
109+
/>
110+
</div>
111+
112+
<div>
113+
<label
114+
htmlFor="pokemon-3"
115+
className="block text-sm font-medium text-gray-700 mb-1"
116+
>
117+
Pokemon 3
118+
</label>
119+
<input
120+
id="pokemon-3"
121+
name="pokemon3"
122+
type="text"
123+
defaultValue=""
124+
className="w-full p-2 border rounded"
125+
placeholder="e.g., Blastoise"
126+
required
127+
/>
128+
</div>
129+
130+
<button
131+
type="submit"
132+
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
133+
>
134+
Register Team
135+
</button>
136+
</form>
137+
138+
{submittedTeam && (
139+
<div className="mt-6 p-4 bg-green-100 rounded">
140+
<h3 className="font-bold text-green-800 mb-2">
141+
Team Registered!
142+
</h3>
143+
<p>
144+
<strong>Trainer:</strong> {submittedTeam.trainerName}
145+
</p>
146+
<p>
147+
<strong>Team:</strong> {submittedTeam.teamName}
148+
</p>
149+
<p>
150+
<strong>Pokemon:</strong> {submittedTeam.pokemon1},{' '}
151+
{submittedTeam.pokemon2}, {submittedTeam.pokemon3}
152+
</p>
153+
</div>
154+
)}
155+
</div>
156+
);
157+
};
158+
159+
export const Final = () => {
160+
return (
161+
<div className="space-y-6">
162+
<PokemonTeamRegistration />
163+
</div>
164+
);
165+
};

0 commit comments

Comments
 (0)