Skip to content

Commit

Permalink
good progress
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Mar 11, 2024
1 parent 30f00c4 commit cd7642a
Show file tree
Hide file tree
Showing 37 changed files with 386 additions and 434 deletions.
88 changes: 41 additions & 47 deletions exercises/01.latest-ref/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,47 @@ that you'd always get the latest value of state or props in your functions.
Let's explore an example:

```tsx
class PokemonFeeder extends React.Component {
state = { selectedPokemonFood: null }
feedPokemon = async () => {
const canEat = await this.props.pokemon.canEat(
this.state.selectedPokemonFood,
)
class PetFeeder extends React.Component {
state = { selectedPetFood: null }
feedPet = async () => {
const canEat = await this.props.pet.canEat(this.state.selectedPetFood)
if (canEat) {
this.props.pokemon.eat(this.state.selectedPokemonFood)
this.props.pet.eat(this.state.selectedPetFood)
}
}
render() {
return (
<div>
<PokemonFoodChooser
onSelection={selectedPokemonFood =>
this.setState({ selectedPokemonFood })
}
<PetFoodChooser
onSelection={selectedPetFood => this.setState({ selectedPetFood })}
/>
<button onClick={this.feedPokemon}>
Feed {this.props.pokemon.name}
</button>
<button onClick={this.feedPet}>Feed {this.props.pet.name}</button>
</div>
)
}
}
```

Think about that `feedPokemon` function for a moment... What kinds of bugs can
you spot with this implementation? Let me ask you a question. What would happen
if the `pokemon.canEat` function took a couple seconds to resolve? What could
the user do to cause a problem? If the user changed the `selectedPokemonFood`
what could happen? Yeah! You could check whether your pidgey can eat worms but
then actually feed it grass! Or what if while we're checking whether charizard
can eat some candy the props changed now we're withholding the candy from a
hungry pikachu! 😢
Think about that `feedPet` function for a moment... What kinds of bugs can you
spot with this implementation? Let me ask you a question. What would happen if
the `pet.canEat` function took a couple seconds to resolve? What could the user
do to cause a problem? If the user changed the `selectedPetFood` what could
happen? Yeah! You could check whether your bird can eat worms but then actually
feed it grass! Or what if while we're checking whether your dog can eat some
candy the props changed now we're withholding the candy from a hungry crab! 😢
(I mean, like crab-candy... I don't know if that's a thing... 😅)

Can you imagine how we could side-step these issues? It's simple actually:

```ts
class PokemonFeeder extends React.Component {
class PetFeeder extends React.Component {
// ...
feedPokemon = async () => {
const { pokemon } = this.props
const { selectedPokemonFood } = this.state
const canEat = await pokemon.canEat(selectedPokemonFood)
feedPet = async () => {
const { pet } = this.props
const { selectedPetFood } = this.state
const canEat = await pet.canEat(selectedPetFood)
if (canEat) {
pokemon.eat(selectedPokemonFood)
pet.eat(selectedPetFood)
}
}
// ...
Expand All @@ -85,18 +79,18 @@ avoid them, so you'll ship fewer (at least, that's been my experience).
So let's rewrite the example above with hooks:

```tsx
function PokemonFeeder({ pokemon }) {
const [selectedPokemonFood, setSelectedPokemonFood] = useState(null)
const feedPokemon = async () => {
const canEat = await pokemon.canEat(selectedPokemonFood)
function PetFeeder({ pet }) {
const [selectedPetFood, setSelectedPetFood] = useState(null)
const feedPet = async () => {
const canEat = await pet.canEat(selectedPetFood)
if (canEat) {
pokemon.eat(selectedPokemonFood)
pet.eat(selectedPetFood)
}
}
return (
<div>
<PokemonFoodChooser onSelection={food => setSelectedPokemonFood(food)} />
<button onClick={feedPokemon}>Feed {pokemon.name}</button>
<PetFoodChooser onSelection={food => setSelectedPetFood(food)} />
<button onClick={feedPet}>Feed {pet.name}</button>
</div>
)
}
Expand All @@ -108,33 +102,33 @@ behavior before? Could we make that work with hooks? Sure! We just need some way
to **ref**erence the latest version of a value. `useRef` to the rescue!

```tsx
function PokemonFeeder({ pokemon }) {
const [selectedPokemonFood, setSelectedPokemonFood] = useState(null)
const latestPokemonRef = useRef(pokemon)
const latestSelectedPokemonFoodRef = useRef(selectedPokemonFood)
function PetFeeder({ pet }) {
const [selectedPetFood, setSelectedPetFood] = useState(null)
const latestPetRef = useRef(pet)
const latestSelectedPetFoodRef = useRef(selectedPetFood)

// why is the useEffect necessary? Because side-effects run in the function
// body of your component can lead to some pretty confusing bugs. Just keep
// your function body free of side-effects and you'll be better off.
useEffect(() => {
latestPokemonRef.current = pokemon
latestSelectedPokemonFoodRef.current = selectedPokemonFood
latestPetRef.current = pet
latestSelectedPetFoodRef.current = selectedPetFood
// Wondering why we have no dependency list? Do we really need it?
// Not really... So we don't bother.
})

const feedPokemon = async () => {
const canEat = await latestPokemonRef.current.canEat(
latestSelectedPokemonFoodRef.current,
const feedPet = async () => {
const canEat = await latestPetRef.current.canEat(
latestSelectedPetFoodRef.current,
)
if (canEat) {
latestPokemonRef.current.eat(latestSelectedPokemonFoodRef.current)
latestPetRef.current.eat(latestSelectedPetFoodRef.current)
}
}
return (
<div>
<PokemonFoodChooser onSelection={food => setSelectedPokemonFood(food)} />
<button onClick={feedPokemon}>Feed {pokemon.name}</button>
<PetFoodChooser onSelection={food => setSelectedPetFood(food)} />
<button onClick={feedPet}>Feed {pet.name}</button>
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion exercises/02.composition/01.problem/README.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Composition and Layout Components

In this exercise we've got a simple user interface with several components
👨‍💼 In this exercise we've got a simple user interface with several components
necessitating passing state and updaters around. We're going to restructure
things so we pass react elements instead of state and updaters. We might be
going a _tiny_ bit overboard, but the goal is for this to be instructive for
Expand Down
30 changes: 17 additions & 13 deletions exercises/02.composition/01.problem/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ nav ul {
padding-left: 0px;
}
nav img {
max-width: 64px;
max-height: 64px;
width: 64px;
height: 64px;
border-radius: 4px;
}
nav a,
Expand Down Expand Up @@ -71,22 +71,22 @@ main {
gap: 4rem;
}

.pokemon-list {
.sport-list {
overflow-y: scroll;
overflow-x: hidden;
max-width: 30vw;
min-width: 200px;
}

.pokemon-list ul {
.sport-list ul {
list-style: none;
padding: 0px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}

.pokemon-list ul li button {
.sport-list ul li button {
display: flex;
gap: 1rem;
align-items: center;
Expand All @@ -98,25 +98,29 @@ main {
border-radius: 2px;
width: 100%;
}
.pokemon-list ul li button small {
.sport-list ul li button small {
position: relative;
}

.pokemon-list .pokemon-list-info {
.sport-list .sport-list-info {
display: flex;
flex-direction: column;
text-align: left;
}

.pokemon-list ul li img {
max-width: 64px;
max-height: 64px;
.sport-list ul li img {
width: 64px;
height: 64px;
border-radius: 4px;
aspect-ratio: 1;
object-fit: contain;
}

.pokemon-details {
min-width: 360px;
min-height: 620px;
.sport-details img {
width: 256px;
height: 256px;
aspect-ratio: 1;
object-fit: contain;
}

footer {
Expand Down
83 changes: 39 additions & 44 deletions exercises/02.composition/01.problem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { useState } from 'react'
import * as ReactDOM from 'react-dom/client'
import { PokemonDataView, allPokemon } from '#shared/pokemon.tsx'
import { type PokemonData, type User } from '#shared/types.tsx'
import { SportDataView, allSports } from '#shared/sports.tsx'
import { type SportData, type User } from '#shared/types.tsx'

function App() {
const [user] = useState<User>({ name: 'Kody', image: '/img/kody.png' })
const [pokemonList] = useState<Array<PokemonData>>(() =>
Object.values(allPokemon),
)
const [selectedPokemon, setSelectedPokemon] = useState<PokemonData | null>(
null,
)
const [sportList] = useState<Array<SportData>>(() => Object.values(allSports))
const [selectedSport, setSelectedSport] = useState<SportData | null>(null)

return (
<div
id="app-root"
style={{ ['--accent-color' as any]: selectedPokemon?.color ?? 'black' }}
style={{ ['--accent-color' as any]: selectedSport?.color ?? 'black' }}
>
{/*
🐨 make Nav accept a ReactElement prop called "avatar"
Expand All @@ -28,9 +24,9 @@ function App() {
instead of the props it accepts right now.
*/}
<Main
pokemonList={pokemonList}
selectedPokemon={selectedPokemon}
setSelectedPokemon={setSelectedPokemon}
sportList={sportList}
selectedSport={selectedSport}
setSelectedSport={setSelectedSport}
/>
<div className="spacer" data-size="lg" />
{/*
Expand Down Expand Up @@ -67,41 +63,41 @@ function Nav({ user }: { user: User }) {

function Main({
// 🐨 all these props should be removed in favor of the sidebar and content props
pokemonList,
selectedPokemon,
setSelectedPokemon,
sportList,
selectedSport,
setSelectedSport,
}: {
pokemonList: Array<PokemonData>
selectedPokemon: PokemonData | null
setSelectedPokemon: (pokemon: PokemonData) => void
sportList: Array<SportData>
selectedSport: SportData | null
setSelectedSport: (sport: SportData) => void
}) {
return (
<main>
{/* 🐨 put the sidebar and content props here */}
<List pokemonList={pokemonList} setSelectedPokemon={setSelectedPokemon} />
<Details selectedPokemon={selectedPokemon} />
<List sportList={sportList} setSelectedSport={setSelectedSport} />
<Details selectedSport={selectedSport} />
</main>
)
}

function List({
// 🐨 make this accept an array of ReactElements called "listItems"
// and remove the existing props
pokemonList,
setSelectedPokemon,
sportList,
setSelectedSport,
}: {
pokemonList: Array<PokemonData>
setSelectedPokemon: (pokemon: PokemonData) => void
sportList: Array<SportData>
setSelectedSport: (sport: SportData) => void
}) {
return (
<div className="pokemon-list">
<div className="sport-list">
<ul>
{/* 🐨 render the listItems here */}
{pokemonList.map(p => (
{sportList.map(p => (
<li key={p.id}>
<PokemonListItemButton
pokemon={p}
onClick={() => setSelectedPokemon(p)}
<SportListItemButton
sport={p}
onClick={() => setSelectedSport(p)}
/>
</li>
))}
Expand All @@ -110,36 +106,35 @@ function List({
)
}

function PokemonListItemButton({
pokemon,
function SportListItemButton({
sport,
onClick,
}: {
pokemon: PokemonData
sport: SportData
onClick: () => void
}) {
return (
<button
className="pokemon-item"
className="sport-item"
onClick={onClick}
style={{ ['--accent-color' as any]: pokemon.color }}
aria-label={pokemon.name}
style={{ ['--accent-color' as any]: sport.color }}
aria-label={sport.name}
>
<img src={pokemon.image} alt={pokemon.name} />
<div className="pokemon-list-info">
<strong>{pokemon.name}</strong>
<small>{`(${pokemon.number})`}</small>
<img src={sport.image} alt={sport.name} />
<div className="sport-list-info">
<strong>{sport.name}</strong>
</div>
</button>
)
}

function Details({ selectedPokemon }: { selectedPokemon: PokemonData | null }) {
function Details({ selectedSport }: { selectedSport: SportData | null }) {
return (
<div className="pokemon-details">
{selectedPokemon ? (
<PokemonDataView pokemon={selectedPokemon} />
<div className="sport-details">
{selectedSport ? (
<SportDataView sport={selectedSport} />
) : (
<div>Select a Pokemon</div>
<div>Select a Sport</div>
)}
</div>
)
Expand Down
8 changes: 4 additions & 4 deletions exercises/02.composition/01.solution/README.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Composition and Layout Components

In this one we didn't actually change any visual behavior (the test was passing
before your changes). But we hopefully demonstrated how restructuring your
components can make it easier to maintain and help you avoid the prop drilling
problem and reduce the amount you feel you need to use `use`.
👨‍💼 In this one we didn't actually change any visual behavior (the test was
passing before your changes). But we hopefully demonstrated how restructuring
your components can make it easier to maintain and help you avoid the prop
drilling problem and reduce the amount you feel you need to use context.
Loading

0 comments on commit cd7642a

Please sign in to comment.