Skip to content

Commit 0fe1465

Browse files
chantastickentcdodds
authored andcommitted
migrate: exercise 06 to kcd-workshop format
1 parent 3d3c2c7 commit 0fe1465

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1022
-374
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# useEffect: HTTP Requests
2+
3+
In this exercise, we'll be doing data fetching directly in a useEffect hook
4+
callback within our component.
5+
6+
Here we have a form where users can enter the name of a pokemon and fetch data
7+
about that pokemon. Your job will be to create a component which makes that
8+
fetch request. When the user submits a pokemon name, our `PokemonInfo` component
9+
will get re-rendered with the `pokemonName`
10+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"

src/exercise/06.tsx renamed to exercises/06.use-effect-http-requests/01.problem/index.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
// useEffect: HTTP requests
2-
// http://localhost:3000/isolated/exercise/06.tsx
3-
41
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
53
// 🐨 you'll want the following additional things from '../pokemon':
64
// fetchPokemon: the function we call to get the pokemon info
75
// PokemonInfoFallback: the thing we show while we're loading the pokemon info
86
// PokemonDataView: the stuff we use to display the pokemon info
9-
import {PokemonForm} from '../pokemon'
7+
import {PokemonForm} from '~/shared/pokemon'
108

119
function PokemonInfo({pokemonName}: {pokemonName: string}) {
1210
// 🐨 Have state for the pokemon (null)
@@ -47,4 +45,6 @@ function App() {
4745
)
4846
}
4947

50-
export {App}
48+
const rootEl = document.createElement('div')
49+
document.body.append(rootEl)
50+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# useEffect: HTTP Requests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"

src/final/06.tsx renamed to exercises/06.use-effect-http-requests/01.solution/index.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
// useEffect: HTTP requests
2-
// http://localhost:3000/isolated/final/06.tsx
3-
41
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
53
import {
64
fetchPokemon,
75
PokemonInfoFallback,
86
PokemonForm,
97
PokemonDataView,
10-
} from '../pokemon'
11-
import type {PokemonData} from '../types'
8+
} from '~/shared/pokemon'
9+
import type {PokemonData} from '~/shared/types'
1210

1311
function PokemonInfo({pokemonName}: {pokemonName: string}) {
1412
const [pokemon, setPokemon] = React.useState<PokemonData | null>(null)
@@ -48,4 +46,6 @@ function App() {
4846
)
4947
}
5048

51-
export {App}
49+
const rootEl = document.createElement('div')
50+
document.body.append(rootEl)
51+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Handle Errors
2+
3+
Unfortunately, sometimes things go wrong and we need to handle errors when they
4+
do so we can show the user useful information. Handle that error and render it
5+
out like so:
6+
7+
```jsx
8+
<div role="alert">
9+
There was an error: <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
10+
</div>
11+
```
12+
13+
You can make an error happen by typing an incorrect pokemon name into the input.
14+
15+
One common question I get about this extra credit is how to handle promise
16+
errors. There are two ways to do it in this extra credit:
17+
18+
```javascript
19+
// option 1: using .catch
20+
fetchPokemon(pokemonName)
21+
.then(pokemon => setPokemon(pokemon))
22+
.catch(error => setError(error))
23+
24+
// option 2: using the second argument to .then
25+
fetchPokemon(pokemonName).then(
26+
pokemon => setPokemon(pokemon),
27+
error => setError(error),
28+
)
29+
```
30+
31+
These are functionally equivalent for our purposes, but they are semantically
32+
different in general.
33+
34+
Using `.catch` means that you'll handle an error in the `fetchPokemon` promise,
35+
but you'll _also_ handle an error in the `setPokemon(pokemon)` call as well.
36+
This is due to the semantics of how promises work.
37+
38+
Using the second argument to `.then` means that you will catch an error that
39+
happens in `fetchPokemon` only. In this case, I knew that calling `setPokemon`
40+
would not throw an error (React handles errors and we have an API to catch those
41+
which we'll use later), so I decided to go with the second argument option.
42+
43+
However, in this situation, it doesn't really make much of a difference. If you
44+
want to go with the safe option, then opt for `.catch`.
45+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
3+
import {
4+
fetchPokemon,
5+
PokemonInfoFallback,
6+
PokemonForm,
7+
PokemonDataView,
8+
} from '~/shared/pokemon'
9+
import type {PokemonData} from '~/shared/types'
10+
11+
function PokemonInfo({pokemonName}: {pokemonName: string}) {
12+
const [pokemon, setPokemon] = React.useState<PokemonData | null>(null)
13+
14+
React.useEffect(() => {
15+
if (!pokemonName) {
16+
return
17+
}
18+
setPokemon(null)
19+
fetchPokemon(pokemonName).then(pokemon => setPokemon(pokemon))
20+
}, [pokemonName])
21+
22+
if (!pokemonName) {
23+
return <span>Submit a pokemon</span>
24+
} else if (!pokemon) {
25+
return <PokemonInfoFallback name={pokemonName} />
26+
} else {
27+
return <PokemonDataView pokemon={pokemon} />
28+
}
29+
}
30+
31+
function App() {
32+
const [pokemonName, setPokemonName] = React.useState('')
33+
34+
function handleSubmit(newPokemonName: string) {
35+
setPokemonName(newPokemonName)
36+
}
37+
38+
return (
39+
<div className="pokemon-info-app">
40+
<PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
41+
<hr />
42+
<div className="pokemon-info">
43+
<PokemonInfo pokemonName={pokemonName} />
44+
</div>
45+
</div>
46+
)
47+
}
48+
49+
const rootEl = document.createElement('div')
50+
document.body.append(rootEl)
51+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Handle Errors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"

src/final/06.extra-1.tsx renamed to exercises/06.use-effect-http-requests/02.solution/index.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
// useEffect: HTTP requests
2-
// 💯 handle errors
3-
// http://localhost:3000/isolated/final/06.extra-1.tsx
4-
51
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
63
import {
74
fetchPokemon,
85
PokemonInfoFallback,
96
PokemonForm,
107
PokemonDataView,
11-
} from '../pokemon'
12-
import type {PokemonData} from '../types'
8+
} from '~/shared/pokemon'
9+
import type {PokemonData} from '~/shared/types'
1310

1411
function PokemonInfo({pokemonName}: {pokemonName: string}) {
1512
const [pokemon, setPokemon] = React.useState<null | PokemonData>(null)
@@ -61,4 +58,6 @@ function App() {
6158
)
6259
}
6360

64-
export {App}
61+
const rootEl = document.createElement('div')
62+
document.body.append(rootEl)
63+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Use A Status
2+
3+
Our logic for what to show the user when is kind of convoluted and requires that
4+
we be really careful about which state we set and when.
5+
6+
We could make things much simpler by having some state to set the explicit
7+
status of our component. Our component can be in the following "states":
8+
9+
- `idle`: no request made yet
10+
- `pending`: request started
11+
- `resolved`: request successful
12+
- `rejected`: request failed
13+
14+
Try to use a status state by setting it to these string values rather than
15+
relying on existing state or booleans.
16+
17+
Learn more about this concept here:
18+
[Stop using isLoading booleans](https://kentcdodds.com/blog/stop-using-isloading-booleans)
19+
20+
💰 Warning: Make sure you call `setPokemon` before calling `setStatus`. We'll
21+
address that more in the next extra credit.
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
3+
import {
4+
fetchPokemon,
5+
PokemonInfoFallback,
6+
PokemonForm,
7+
PokemonDataView,
8+
} from '~/shared/pokemon'
9+
import type {PokemonData} from '~/shared/types'
10+
11+
function PokemonInfo({pokemonName}: {pokemonName: string}) {
12+
const [pokemon, setPokemon] = React.useState<null | PokemonData>(null)
13+
const [error, setError] = React.useState<null | Error>(null)
14+
15+
React.useEffect(() => {
16+
if (!pokemonName) {
17+
return
18+
}
19+
setPokemon(null)
20+
setError(null)
21+
fetchPokemon(pokemonName).then(
22+
pokemon => setPokemon(pokemon),
23+
error => setError(error),
24+
)
25+
}, [pokemonName])
26+
27+
if (error) {
28+
return (
29+
<div role="alert">
30+
There was an error:{' '}
31+
<pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
32+
</div>
33+
)
34+
} else if (!pokemonName) {
35+
return <span>Submit a pokemon</span>
36+
} else if (!pokemon) {
37+
return <PokemonInfoFallback name={pokemonName} />
38+
} else {
39+
return <PokemonDataView pokemon={pokemon} />
40+
}
41+
}
42+
43+
function App() {
44+
const [pokemonName, setPokemonName] = React.useState('')
45+
46+
function handleSubmit(newPokemonName: string) {
47+
setPokemonName(newPokemonName)
48+
}
49+
50+
return (
51+
<div className="pokemon-info-app">
52+
<PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
53+
<hr />
54+
<div className="pokemon-info">
55+
<PokemonInfo pokemonName={pokemonName} />
56+
</div>
57+
</div>
58+
)
59+
}
60+
61+
const rootEl = document.createElement('div')
62+
document.body.append(rootEl)
63+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Use A Status
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"

src/final/06.extra-2.tsx renamed to exercises/06.use-effect-http-requests/03.solution/index.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
// useEffect: HTTP requests
2-
// 💯 use a status
3-
// http://localhost:3000/isolated/final/06.extra-2.tsx
4-
51
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom/client'
63
import {
74
fetchPokemon,
85
PokemonInfoFallback,
96
PokemonForm,
107
PokemonDataView,
11-
} from '../pokemon'
12-
import type {PokemonData} from '../types'
8+
} from '~/shared/pokemon'
9+
import type {PokemonData} from '~/shared/types'
1310

1411
function PokemonInfo({pokemonName}: {pokemonName: string}) {
1512
const [status, setStatus] = React.useState('idle')
@@ -69,4 +66,6 @@ function App() {
6966
)
7067
}
7168

72-
export {App}
69+
const rootEl = document.createElement('div')
70+
document.body.append(rootEl)
71+
ReactDOM.createRoot(rootEl).render(<App />)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Store the State in an Object
2+
3+
You'll notice that we're calling a bunch of state updaters in a row. This is
4+
normally not a problem, but each call to our state updater can result in a
5+
re-render of our component. React normally batches these calls so you only get a
6+
single re-render, but it's unable to do this in an asynchronous callback (like
7+
our promise success and error handlers).
8+
9+
So you might notice that if you do this:
10+
11+
```javascript
12+
setStatus('resolved')
13+
setPokemon(pokemon)
14+
```
15+
16+
You'll get an error indicating that you cannot read `image` of `null`. This is
17+
because the `setStatus` call results in a re-render that happens before the
18+
`setPokemon` happens.
19+
20+
> but it's unable to do this in an asynchronous callback
21+
22+
This is no longer the case in React 18 as it supports automatic batching for
23+
asynchronous callback too.
24+
25+
Learn more about this concept here:
26+
[New Feature: Automatic Batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching)
27+
28+
Still it is better to maintain closely related states as an object rather than
29+
maintaining them using individual useState hooks.
30+
31+
Learn more about this concept here:
32+
[Should I useState or useReducer?](https://kentcdodds.com/blog/should-i-usestate-or-usereducer#conclusion)
33+
34+
In the future, you'll learn about how `useReducer` can solve this problem really
35+
elegantly, but we can still accomplish this by storing our state as an object
36+
that has all the properties of state we're managing.
37+
38+
See if you can figure out how to store all of your state in a single object with
39+
a single `React.useState` call so I can update my state like this:
40+
41+
```javascript
42+
setState({status: 'resolved', pokemon})
43+
```
44+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "/pokemon.css"

0 commit comments

Comments
 (0)