77 | )
78 | }
79 |
80 | export default App
81 |
--------------------------------------------------------------------------------
/src/exercise/03.md:
--------------------------------------------------------------------------------
1 | # Lifting state
2 |
3 | ## Recordings
4 | [Exercise](https://drive.google.com/file/d/1IgINnQD3vxXRQTZpE3IKmYOiEqgR2H6V/view?usp=sharing)
5 | [Extra Credit 1](https://drive.google.com/file/d/1vP7CTwuvomkt3rFSfJOZYZBjHSguV1S2/view?usp=sharing)
6 | ## 📝 Your Notes
7 |
8 | Elaborate on your learnings here in `src/exercise/03.md`
9 |
10 | ## Background
11 |
12 | A common question from React beginners is how to share state between two sibling
13 | components. The answer is to
14 | ["lift the state"](https://reactjs.org/docs/lifting-state-up.html) which
15 | basically amounts to finding the lowest common parent shared between the two
16 | components and placing the state management there, and then passing the state
17 | and a mechanism for updating that state down into the components that need it.
18 |
19 | ## Exercise
20 |
21 | Production deploys:
22 |
23 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/03.js)
24 | - [Final](https://react-hooks.netlify.app/isolated/final/03.js)
25 |
26 | 👨💼 Peter told us we've got a new feature request for the `Display` component. He
27 | wants us to display the `animal` the user selects. But that state is managed in
28 | a "sibling" component, so we have to move that management to the least common
29 | parent (`App`) and then pass it down.
30 |
31 | ## Extra Credit
32 |
33 | ### 1. 💯 colocating state
34 |
35 | [Production deploy](https://react-hooks.netlify.app/isolated/final/03.extra-1.js)
36 |
37 | As a community we’re pretty good at lifting state. It becomes natural over time.
38 | One thing that we typically have trouble remembering to do is to push state back
39 | down (or
40 | [colocate state](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster)).
41 |
42 | 👨💼 Peter told us that now users only want the animal displayed instead of the
43 | name:
44 |
45 | ```javascript
46 | function Display({animal}) {
47 | return
{`Your favorite animal is: ${animal}!`}
48 | }
49 | ```
50 |
51 | You'll notice that just updating the `Display` component to this works fine, but
52 | for the extra credit, go through the process of moving state to the components
53 | that need it. You know what you just did for the `Animal` component? You need to
54 | do the opposite thing for the `Name` component.
55 |
56 | ## 🦉 Feedback
57 |
58 | Fill out
59 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=03%3A%20Lifting%20state&em=).
60 |
--------------------------------------------------------------------------------
/src/final/06.extra-7.js:
--------------------------------------------------------------------------------
1 | // useEffect: HTTP requests
2 | // 💯 reset the error boundary
3 | // http://localhost:3000/isolated/final/06.extra-7.js
4 |
5 | import * as React from 'react'
6 | import {ErrorBoundary} from 'react-error-boundary'
7 | import {
8 | fetchPokemon,
9 | PokemonInfoFallback,
10 | PokemonForm,
11 | PokemonDataView,
12 | } from '../pokemon'
13 |
14 | function PokemonInfo({pokemonName}) {
15 | const [state, setState] = React.useState({
16 | status: pokemonName ? 'pending' : 'idle',
17 | pokemon: null,
18 | error: null,
19 | })
20 | const {status, pokemon, error} = state
21 |
22 | React.useEffect(() => {
23 | if (!pokemonName) {
24 | return
25 | }
26 | setState({status: 'pending'})
27 | fetchPokemon(pokemonName).then(
28 | pokemon => {
29 | setState({status: 'resolved', pokemon})
30 | },
31 | error => {
32 | setState({status: 'rejected', error})
33 | },
34 | )
35 | }, [pokemonName])
36 |
37 | if (status === 'idle') {
38 | return 'Submit a pokemon'
39 | } else if (status === 'pending') {
40 | return
41 | } else if (status === 'rejected') {
42 | // this will be handled by an error boundary
43 | throw error
44 | } else if (status === 'resolved') {
45 | return
46 | }
47 |
48 | throw new Error('This should be impossible')
49 | }
50 |
51 | function ErrorFallback({error, resetErrorBoundary}) {
52 | return (
53 |
66 | )
67 | }
68 |
69 | function calculateStatus(winner, squares, nextValue) {
70 | return winner
71 | ? `Winner: ${winner}`
72 | : squares.every(Boolean)
73 | ? `Scratch: Cat's game`
74 | : `Next player: ${nextValue}`
75 | }
76 |
77 | function calculateNextValue(squares) {
78 | return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
79 | }
80 |
81 | function calculateWinner(squares) {
82 | const lines = [
83 | [0, 1, 2],
84 | [3, 4, 5],
85 | [6, 7, 8],
86 | [0, 3, 6],
87 | [1, 4, 7],
88 | [2, 5, 8],
89 | [0, 4, 8],
90 | [2, 4, 6],
91 | ]
92 | for (let i = 0; i < lines.length; i++) {
93 | const [a, b, c] = lines[i]
94 | if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
95 | return squares[a]
96 | }
97 | }
98 | return null
99 | }
100 |
101 | function App() {
102 | return
103 | }
104 |
105 | export default App
106 |
--------------------------------------------------------------------------------
/src/exercise/05.md:
--------------------------------------------------------------------------------
1 | # useRef and useEffect: DOM interaction
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/05.md`
6 |
7 | ## Background
8 |
9 | Often when working with React you'll need to integrate with UI libraries. Some
10 | of these need to work directly with the DOM. Remember that when you do:
11 | `
hi
` that's actually syntactic sugar for a `React.createElement` so
12 | you don't actually have access to DOM nodes in your render method. In fact, DOM
13 | nodes aren't created at all until the `ReactDOM.render` method is called. Your
14 | component's render method is really just responsible for creating and returning
15 | React Elements and has nothing to do with the DOM in particular.
16 |
17 | So to get access to the DOM, you need to ask React to give you access to a
18 | particular DOM node when it renders your component. The way this happens is
19 | through a special prop called `ref`.
20 |
21 | Here's a simple example of using the `ref` prop:
22 |
23 | ```javascript
24 | function MyDiv() {
25 | const myDivRef = React.useRef()
26 | React.useEffect(() => {
27 | const myDiv = myDivRef.current
28 | // myDiv is the div DOM node!
29 | console.log(myDiv)
30 | }, [])
31 | return
hi
32 | }
33 | ```
34 |
35 | After the component has been rendered, it's considered "mounted." That's when
36 | the React.useEffect callback is called and so by that point, the ref should have
37 | its `current` property set to the DOM node. So often you'll do direct DOM
38 | interactions/manipulations in the `useEffect` callback.
39 |
40 | ## Exercise
41 |
42 | Production deploys:
43 |
44 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/05.js)
45 | - [Final](https://react-hooks.netlify.app/isolated/final/05.js)
46 |
47 | In this exercise we're going to make a `` component that renders a div
48 | and uses the `vanilla-tilt` library to make it super fancy.
49 |
50 | The thing is, `vanilla-tilt` works directly with DOM nodes to setup event
51 | handlers and stuff, so we need access to the DOM node. But because we're not the
52 | one calling `document.createElement` (React does) we need React to give it to
53 | us.
54 |
55 | So in this exercise we're going to use a `ref` so React can give us the DOM node
56 | and then we can pass that on to `vanilla-tilt`.
57 |
58 | Additionally, we'll need to clean up after ourselves if this component is
59 | unmounted. Otherwise we'll have event handlers dangling around on DOM nodes that
60 | are no longer in the document.
61 |
62 | ### Alternate:
63 |
64 | If you'd prefer to practice refactoring a class that does this to a hook, then
65 | you can open `src/exercise/05-classes.js` and open that on
66 | [an isolated page](http://localhost:3000/isolated/exercise/05-classes.js) to
67 | practice that.
68 |
69 | ## 🦉 Feedback
70 |
71 | Fill out
72 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=05%3A%20useRef%20and%20useEffect%3A%20DOM%20interaction&em=).
73 |
--------------------------------------------------------------------------------
/src/final/04.extra-2.js:
--------------------------------------------------------------------------------
1 | // useState: tic tac toe
2 | // 💯 useLocalStorageState
3 | // http://localhost:3000/isolated/final/04.extra-2.js
4 |
5 | import * as React from 'react'
6 | import {useLocalStorageState} from '../utils'
7 |
8 | function Board() {
9 | const [squares, setSquares] = useLocalStorageState(
10 | 'squares',
11 | Array(9).fill(null),
12 | )
13 |
14 | const nextValue = calculateNextValue(squares)
15 | const winner = calculateWinner(squares)
16 | const status = calculateStatus(winner, squares, nextValue)
17 |
18 | function selectSquare(square) {
19 | if (winner || squares[square]) {
20 | return
21 | }
22 | const squaresCopy = [...squares]
23 | squaresCopy[square] = nextValue
24 | setSquares(squaresCopy)
25 | }
26 |
27 | function restart() {
28 | setSquares(Array(9).fill(null))
29 | }
30 |
31 | function renderSquare(i) {
32 | return (
33 |
36 | )
37 | }
38 |
39 | return (
40 |
119 | >
120 | )
121 |
122 | console.log('%cApp: render end', 'color: MediumSpringGreen')
123 |
124 | return element
125 | }
126 |
127 | export default App
128 |
--------------------------------------------------------------------------------
/src/exercise/01.md:
--------------------------------------------------------------------------------
1 | # useState: greeting
2 |
3 | ## Recordings
4 | [Exercise](https://drive.google.com/file/d/1tspXVGfFOYTH63B4ZirYnGhq_UUy7ocs/view?usp=sharing)
5 | [Extra Credit 1](https://drive.google.com/file/d/1jDc87PseaNkCHSZIAEdbo2sO8yMxkcqe/view?usp=sharing)
6 | ## 📝 Your Notes
7 |
8 | Elaborate on your learnings here in `src/exercise/01.md`
9 |
10 | ## Background
11 |
12 | Normally an interactive application will need to hold state somewhere. In React,
13 | you use special functions called "hooks" to do this. Common built-in hooks
14 | include:
15 |
16 | - `React.useState`
17 | - `React.useEffect`
18 | - `React.useContext`
19 | - `React.useRef`
20 | - `React.useReducer`
21 |
22 | Each of these is a special function that you can call inside your custom React
23 | component function to store data (like state) or perform actions (or
24 | side-effects). There are a few more built-in hooks that have special use cases,
25 | but the ones above are what you'll be using most of the time.
26 |
27 | Each of the hooks has a unique API. Some return a value (like `React.useRef` and
28 | `React.useContext`), others return a pair of values (like `React.useState` and
29 | `React.useReducer`), and others return nothing at all (like `React.useEffect`).
30 |
31 | Here's an example of a component that uses the `useState` hook and an onClick
32 | event handler to update that state:
33 |
34 | ```jsx
35 | function Counter() {
36 | const [count, setCount] = React.useState(0)
37 | const increment = () => setCount(count + 1)
38 | return
39 | }
40 | ```
41 |
42 | `React.useState` is a function that accepts a single argument. That argument is
43 | the initial state for the instance of the component. In our case, the state will
44 | start as `0`.
45 |
46 | `React.useState` returns a pair of values. It does this by returning an array
47 | with two elements (and we use destructuring syntax to assign each of those
48 | values to distinct variables). The first of the pair is the state value and the
49 | second is a function we can call to update the state. We can name these
50 | variables whatever we want. Common convention is to choose a name for the state
51 | variable, then prefix `set` in front of that for the updater function.
52 |
53 | State can be defined as: data that changes over time. So how does this work over
54 | time? When the button is clicked, our `increment` function will be called at
55 | which time we update the `count` by calling `setCount`.
56 |
57 | When we call `setCount`, that tells React to re-render our component. When it
58 | does this, the entire `Counter` function is re-run, so when `React.useState` is
59 | called this time, the value we get back is the value that we called `setCount`
60 | with. And it continues like that until `Counter` is unmounted (removed from the
61 | application), or the user closes the application.
62 |
63 | ## Exercise
64 |
65 | Production deploys:
66 |
67 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/01.js)
68 | - [Final](https://react-hooks.netlify.app/isolated/final/01.js)
69 |
70 | In this exercise we have a form where you can type in your name and it will give
71 | you a greeting as you type. Fill out the `Greeting` component so that it manages
72 | the state of the name and shows the greeting as the name is changed.
73 |
74 | ## Extra Credit
75 |
76 | ### 1. 💯 accept an initialName
77 |
78 | [Production deploy](https://react-hooks.netlify.app/isolated/final/01.extra-1.js)
79 |
80 | Make the `Greeting` accept a prop called `initialName` and initialize the `name`
81 | state to that value.
82 |
83 | ## 🦉 Feedback
84 |
85 | Fill out
86 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=01%3A%20useState%3A%20greeting&em=).
87 |
--------------------------------------------------------------------------------
/src/exercise/04-classes.js:
--------------------------------------------------------------------------------
1 | // useState: tic tac toe
2 | // 💯 (alternate) migrate from classes
3 | // http://localhost:3000/isolated/exercise/04-classes.js
4 |
5 | import * as React from 'react'
6 |
7 | // If you'd rather practice refactoring a class component to a function
8 | // component with hooks, then go ahead and do this exercise.
9 |
10 | // 🦉 You've learned all the hooks you need to know to refactor this Board
11 | // component to hooks. So, let's make it happen!
12 |
13 | class Board extends React.Component {
14 | state = {
15 | squares:
16 | JSON.parse(window.localStorage.getItem('squares')) || Array(9).fill(null),
17 | }
18 |
19 | selectSquare(square) {
20 | const {squares} = this.state
21 | const nextValue = calculateNextValue(squares)
22 | if (calculateWinner(squares) || squares[square]) {
23 | return
24 | }
25 | const squaresCopy = [...squares]
26 | squaresCopy[square] = nextValue
27 | this.setState({squares: squaresCopy})
28 | }
29 | renderSquare = i => (
30 |
33 | )
34 |
35 | restart = () => {
36 | this.setState({squares: Array(9).fill(null)})
37 | this.updateLocalStorage()
38 | }
39 |
40 | componentDidMount() {
41 | this.updateLocalStorage()
42 | }
43 |
44 | componentDidUpdate(prevProps, prevState) {
45 | if (prevState.squares !== this.state.squares) {
46 | this.updateLocalStorage()
47 | }
48 | }
49 |
50 | updateLocalStorage() {
51 | window.localStorage.setItem('squares', JSON.stringify(this.state.squares))
52 | }
53 |
54 | render() {
55 | const {squares} = this.state
56 | const nextValue = calculateNextValue(squares)
57 | const winner = calculateWinner(squares)
58 | let status = calculateStatus(winner, squares, nextValue)
59 |
60 | return (
61 |
105 | )
106 | }
107 |
108 | function PokemonForm({
109 | pokemonName: externalPokemonName,
110 | initialPokemonName = externalPokemonName || '',
111 | onSubmit,
112 | }) {
113 | const [pokemonName, setPokemonName] = React.useState(initialPokemonName)
114 |
115 | // this is generally not a great idea. We're synchronizing state when it is
116 | // normally better to derive it https://kentcdodds.com/blog/dont-sync-state-derive-it
117 | // however, we're doing things this way to make it easier for the exercises
118 | // to not have to worry about the logic for this PokemonForm component.
119 | React.useEffect(() => {
120 | // note that because it's a string value, if the externalPokemonName
121 | // is the same as the one we're managing, this will not trigger a re-render
122 | if (typeof externalPokemonName === 'string') {
123 | setPokemonName(externalPokemonName)
124 | }
125 | }, [externalPokemonName])
126 |
127 | function handleChange(e) {
128 | setPokemonName(e.target.value)
129 | }
130 |
131 | function handleSubmit(e) {
132 | e.preventDefault()
133 | onSubmit(pokemonName)
134 | }
135 |
136 | function handleSelect(newPokemonName) {
137 | setPokemonName(newPokemonName)
138 | onSubmit(newPokemonName)
139 | }
140 |
141 | return (
142 |
184 | )
185 | }
186 |
187 | function ErrorFallback({error, resetErrorBoundary}) {
188 | return (
189 |
190 | There was an error:{' '}
191 |
{error.message}
192 |
193 |
194 | )
195 | }
196 |
197 | function PokemonErrorBoundary(props) {
198 | return
199 | }
200 |
201 | export {
202 | PokemonInfoFallback,
203 | PokemonForm,
204 | PokemonDataView,
205 | fetchPokemon,
206 | PokemonErrorBoundary,
207 | }
208 |
--------------------------------------------------------------------------------
/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Mock Service Worker.
3 | * @see https://github.com/mswjs/msw
4 | * - Please do NOT modify this file.
5 | * - Please do NOT serve this file on production.
6 | */
7 | /* eslint-disable */
8 | /* tslint:disable */
9 |
10 | const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
11 | const bypassHeaderName = 'x-msw-bypass'
12 | const activeClientIds = new Set()
13 |
14 | self.addEventListener('install', function () {
15 | return self.skipWaiting()
16 | })
17 |
18 | self.addEventListener('activate', async function (event) {
19 | return self.clients.claim()
20 | })
21 |
22 | self.addEventListener('message', async function (event) {
23 | const clientId = event.source.id
24 |
25 | if (!clientId || !self.clients) {
26 | return
27 | }
28 |
29 | const client = await self.clients.get(clientId)
30 |
31 | if (!client) {
32 | return
33 | }
34 |
35 | const allClients = await self.clients.matchAll()
36 |
37 | switch (event.data) {
38 | case 'KEEPALIVE_REQUEST': {
39 | sendToClient(client, {
40 | type: 'KEEPALIVE_RESPONSE',
41 | })
42 | break
43 | }
44 |
45 | case 'INTEGRITY_CHECK_REQUEST': {
46 | sendToClient(client, {
47 | type: 'INTEGRITY_CHECK_RESPONSE',
48 | payload: INTEGRITY_CHECKSUM,
49 | })
50 | break
51 | }
52 |
53 | case 'MOCK_ACTIVATE': {
54 | activeClientIds.add(clientId)
55 |
56 | sendToClient(client, {
57 | type: 'MOCKING_ENABLED',
58 | payload: true,
59 | })
60 | break
61 | }
62 |
63 | case 'MOCK_DEACTIVATE': {
64 | activeClientIds.delete(clientId)
65 | break
66 | }
67 |
68 | case 'CLIENT_CLOSED': {
69 | activeClientIds.delete(clientId)
70 |
71 | const remainingClients = allClients.filter((client) => {
72 | return client.id !== clientId
73 | })
74 |
75 | // Unregister itself when there are no more clients
76 | if (remainingClients.length === 0) {
77 | self.registration.unregister()
78 | }
79 |
80 | break
81 | }
82 | }
83 | })
84 |
85 | // Resolve the "master" client for the given event.
86 | // Client that issues a request doesn't necessarily equal the client
87 | // that registered the worker. It's with the latter the worker should
88 | // communicate with during the response resolving phase.
89 | async function resolveMasterClient(event) {
90 | const client = await self.clients.get(event.clientId)
91 |
92 | if (client.frameType === 'top-level') {
93 | return client
94 | }
95 |
96 | const allClients = await self.clients.matchAll()
97 |
98 | return allClients
99 | .filter((client) => {
100 | // Get only those clients that are currently visible.
101 | return client.visibilityState === 'visible'
102 | })
103 | .find((client) => {
104 | // Find the client ID that's recorded in the
105 | // set of clients that have registered the worker.
106 | return activeClientIds.has(client.id)
107 | })
108 | }
109 |
110 | async function handleRequest(event, requestId) {
111 | const client = await resolveMasterClient(event)
112 | const response = await getResponse(event, client, requestId)
113 |
114 | // Send back the response clone for the "response:*" life-cycle events.
115 | // Ensure MSW is active and ready to handle the message, otherwise
116 | // this message will pend indefinitely.
117 | if (client && activeClientIds.has(client.id)) {
118 | ;(async function () {
119 | const clonedResponse = response.clone()
120 | sendToClient(client, {
121 | type: 'RESPONSE',
122 | payload: {
123 | requestId,
124 | type: clonedResponse.type,
125 | ok: clonedResponse.ok,
126 | status: clonedResponse.status,
127 | statusText: clonedResponse.statusText,
128 | body:
129 | clonedResponse.body === null ? null : await clonedResponse.text(),
130 | headers: serializeHeaders(clonedResponse.headers),
131 | redirected: clonedResponse.redirected,
132 | },
133 | })
134 | })()
135 | }
136 |
137 | return response
138 | }
139 |
140 | async function getResponse(event, client, requestId) {
141 | const { request } = event
142 | const requestClone = request.clone()
143 | const getOriginalResponse = () => fetch(requestClone)
144 |
145 | // Bypass mocking when the request client is not active.
146 | if (!client) {
147 | return getOriginalResponse()
148 | }
149 |
150 | // Bypass initial page load requests (i.e. static assets).
151 | // The absence of the immediate/parent client in the map of the active clients
152 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
153 | // and is not ready to handle requests.
154 | if (!activeClientIds.has(client.id)) {
155 | return await getOriginalResponse()
156 | }
157 |
158 | // Bypass requests with the explicit bypass header
159 | if (requestClone.headers.get(bypassHeaderName) === 'true') {
160 | const cleanRequestHeaders = serializeHeaders(requestClone.headers)
161 |
162 | // Remove the bypass header to comply with the CORS preflight check.
163 | delete cleanRequestHeaders[bypassHeaderName]
164 |
165 | const originalRequest = new Request(requestClone, {
166 | headers: new Headers(cleanRequestHeaders),
167 | })
168 |
169 | return fetch(originalRequest)
170 | }
171 |
172 | // Send the request to the client-side MSW.
173 | const reqHeaders = serializeHeaders(request.headers)
174 | const body = await request.text()
175 |
176 | const clientMessage = await sendToClient(client, {
177 | type: 'REQUEST',
178 | payload: {
179 | id: requestId,
180 | url: request.url,
181 | method: request.method,
182 | headers: reqHeaders,
183 | cache: request.cache,
184 | mode: request.mode,
185 | credentials: request.credentials,
186 | destination: request.destination,
187 | integrity: request.integrity,
188 | redirect: request.redirect,
189 | referrer: request.referrer,
190 | referrerPolicy: request.referrerPolicy,
191 | body,
192 | bodyUsed: request.bodyUsed,
193 | keepalive: request.keepalive,
194 | },
195 | })
196 |
197 | switch (clientMessage.type) {
198 | case 'MOCK_SUCCESS': {
199 | return delayPromise(
200 | () => respondWithMock(clientMessage),
201 | clientMessage.payload.delay,
202 | )
203 | }
204 |
205 | case 'MOCK_NOT_FOUND': {
206 | return getOriginalResponse()
207 | }
208 |
209 | case 'NETWORK_ERROR': {
210 | const { name, message } = clientMessage.payload
211 | const networkError = new Error(message)
212 | networkError.name = name
213 |
214 | // Rejecting a request Promise emulates a network error.
215 | throw networkError
216 | }
217 |
218 | case 'INTERNAL_ERROR': {
219 | const parsedBody = JSON.parse(clientMessage.payload.body)
220 |
221 | console.error(
222 | `\
223 | [MSW] Request handler function for "%s %s" has thrown the following exception:
224 |
225 | ${parsedBody.errorType}: ${parsedBody.message}
226 | (see more detailed error stack trace in the mocked response body)
227 |
228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
229 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
230 | `,
231 | request.method,
232 | request.url,
233 | )
234 |
235 | return respondWithMock(clientMessage)
236 | }
237 | }
238 |
239 | return getOriginalResponse()
240 | }
241 |
242 | self.addEventListener('fetch', function (event) {
243 | const { request } = event
244 |
245 | // Bypass navigation requests.
246 | if (request.mode === 'navigate') {
247 | return
248 | }
249 |
250 | // Opening the DevTools triggers the "only-if-cached" request
251 | // that cannot be handled by the worker. Bypass such requests.
252 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
253 | return
254 | }
255 |
256 | // Bypass all requests when there are no active clients.
257 | // Prevents the self-unregistered worked from handling requests
258 | // after it's been deleted (still remains active until the next reload).
259 | if (activeClientIds.size === 0) {
260 | return
261 | }
262 |
263 | const requestId = uuidv4()
264 |
265 | return event.respondWith(
266 | handleRequest(event, requestId).catch((error) => {
267 | console.error(
268 | '[MSW] Failed to mock a "%s" request to "%s": %s',
269 | request.method,
270 | request.url,
271 | error,
272 | )
273 | }),
274 | )
275 | })
276 |
277 | function serializeHeaders(headers) {
278 | const reqHeaders = {}
279 | headers.forEach((value, name) => {
280 | reqHeaders[name] = reqHeaders[name]
281 | ? [].concat(reqHeaders[name]).concat(value)
282 | : value
283 | })
284 | return reqHeaders
285 | }
286 |
287 | function sendToClient(client, message) {
288 | return new Promise((resolve, reject) => {
289 | const channel = new MessageChannel()
290 |
291 | channel.port1.onmessage = (event) => {
292 | if (event.data && event.data.error) {
293 | return reject(event.data.error)
294 | }
295 |
296 | resolve(event.data)
297 | }
298 |
299 | client.postMessage(JSON.stringify(message), [channel.port2])
300 | })
301 | }
302 |
303 | function delayPromise(cb, duration) {
304 | return new Promise((resolve) => {
305 | setTimeout(() => resolve(cb()), duration)
306 | })
307 | }
308 |
309 | function respondWithMock(clientMessage) {
310 | return new Response(clientMessage.payload.body, {
311 | ...clientMessage.payload,
312 | headers: clientMessage.payload.headers,
313 | })
314 | }
315 |
316 | function uuidv4() {
317 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
318 | const r = (Math.random() * 16) | 0
319 | const v = c == 'x' ? r : (r & 0x3) | 0x8
320 | return v.toString(16)
321 | })
322 | }
323 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "react-hooks",
3 | "projectOwner": "kentcdodds",
4 | "repoType": "github",
5 | "files": [
6 | "README.md"
7 | ],
8 | "imageSize": 100,
9 | "commit": false,
10 | "contributors": [
11 | {
12 | "login": "kentcdodds",
13 | "name": "Kent C. Dodds",
14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
15 | "profile": "https://kentcdodds.com",
16 | "contributions": [
17 | "code",
18 | "doc",
19 | "infra",
20 | "test"
21 | ]
22 | },
23 | {
24 | "login": "tsnieman",
25 | "name": "Tyler Nieman",
26 | "avatar_url": "https://avatars3.githubusercontent.com/u/595711?v=4",
27 | "profile": "http://tsnieman.net/",
28 | "contributions": [
29 | "code",
30 | "doc"
31 | ]
32 | },
33 | {
34 | "login": "mplis",
35 | "name": "Mike Plis",
36 | "avatar_url": "https://avatars0.githubusercontent.com/u/1382377?v=4",
37 | "profile": "https://github.com/mplis",
38 | "contributions": [
39 | "code",
40 | "test"
41 | ]
42 | },
43 | {
44 | "login": "jdorfman",
45 | "name": "Justin Dorfman",
46 | "avatar_url": "https://avatars1.githubusercontent.com/u/398230?v=4",
47 | "profile": "https://stackshare.io/jdorfman/decisions",
48 | "contributions": [
49 | "fundingFinding"
50 | ]
51 | },
52 | {
53 | "login": "AlgusDark",
54 | "name": "Carlos Pérez Gutiérrez",
55 | "avatar_url": "https://avatars1.githubusercontent.com/u/818856?v=4",
56 | "profile": "http://algus.ninja",
57 | "contributions": [
58 | "code"
59 | ]
60 | },
61 | {
62 | "login": "CharlieStras",
63 | "name": "Charlie Stras",
64 | "avatar_url": "https://avatars2.githubusercontent.com/u/10193500?v=4",
65 | "profile": "http://charliestras.me",
66 | "contributions": [
67 | "doc",
68 | "code"
69 | ]
70 | },
71 | {
72 | "login": "lideo",
73 | "name": "Lide",
74 | "avatar_url": "https://avatars3.githubusercontent.com/u/1573567?v=4",
75 | "profile": "https://github.com/lideo",
76 | "contributions": [
77 | "doc"
78 | ]
79 | },
80 | {
81 | "login": "marcosvega91",
82 | "name": "Marco Moretti",
83 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4",
84 | "profile": "https://github.com/marcosvega91",
85 | "contributions": [
86 | "code"
87 | ]
88 | },
89 | {
90 | "login": "gugol2",
91 | "name": "Watchmaker",
92 | "avatar_url": "https://avatars0.githubusercontent.com/u/4933016?v=4",
93 | "profile": "https://github.com/gugol2",
94 | "contributions": [
95 | "bug"
96 | ]
97 | },
98 | {
99 | "login": "dschapman",
100 | "name": "Daniel Chapman",
101 | "avatar_url": "https://avatars3.githubusercontent.com/u/36767987?v=4",
102 | "profile": "https://dschapman.com",
103 | "contributions": [
104 | "code"
105 | ]
106 | },
107 | {
108 | "login": "flofehrenbacher",
109 | "name": "flofehrenbacher",
110 | "avatar_url": "https://avatars0.githubusercontent.com/u/18660708?v=4",
111 | "profile": "https://github.com/flofehrenbacher",
112 | "contributions": [
113 | "doc"
114 | ]
115 | },
116 | {
117 | "login": "PritamSangani",
118 | "name": "Pritam Sangani",
119 | "avatar_url": "https://avatars3.githubusercontent.com/u/22857896?v=4",
120 | "profile": "https://www.linkedin.com/in/pritamsangani/",
121 | "contributions": [
122 | "code"
123 | ]
124 | },
125 | {
126 | "login": "emzoumpo",
127 | "name": "Emmanouil Zoumpoulakis",
128 | "avatar_url": "https://avatars2.githubusercontent.com/u/2103443?v=4",
129 | "profile": "https://github.com/emzoumpo",
130 | "contributions": [
131 | "doc"
132 | ]
133 | },
134 | {
135 | "login": "Aprillion",
136 | "name": "Peter Hozák",
137 | "avatar_url": "https://avatars0.githubusercontent.com/u/1087670?v=4",
138 | "profile": "http://peter.hozak.info/",
139 | "contributions": [
140 | "code",
141 | "doc"
142 | ]
143 | },
144 | {
145 | "login": "timobleeker",
146 | "name": "Timo",
147 | "avatar_url": "https://avatars0.githubusercontent.com/u/2723586?v=4",
148 | "profile": "https://github.com/timobleeker",
149 | "contributions": [
150 | "doc"
151 | ]
152 | },
153 | {
154 | "login": "thacherhussain",
155 | "name": "Thacher Hussain",
156 | "avatar_url": "https://avatars1.githubusercontent.com/u/12368025?v=4",
157 | "profile": "http://thacher.co",
158 | "contributions": [
159 | "doc"
160 | ]
161 | },
162 | {
163 | "login": "jmagrippis",
164 | "name": "Johnny Magrippis",
165 | "avatar_url": "https://avatars0.githubusercontent.com/u/3502800?v=4",
166 | "profile": "https://magrippis.com",
167 | "contributions": [
168 | "code"
169 | ]
170 | },
171 | {
172 | "login": "apolakipso",
173 | "name": "Apola Kipso",
174 | "avatar_url": "https://avatars2.githubusercontent.com/u/494674?v=4",
175 | "profile": "https://twitter.com/apolakipso",
176 | "contributions": [
177 | "code"
178 | ]
179 | },
180 | {
181 | "login": "Snaptags",
182 | "name": "Markus Lasermann",
183 | "avatar_url": "https://avatars1.githubusercontent.com/u/1249745?v=4",
184 | "profile": "https://github.com/Snaptags",
185 | "contributions": [
186 | "test"
187 | ]
188 | },
189 | {
190 | "login": "degeens",
191 | "name": "Stijn Geens",
192 | "avatar_url": "https://avatars2.githubusercontent.com/u/33414262?v=4",
193 | "profile": "https://github.com/degeens",
194 | "contributions": [
195 | "doc"
196 | ]
197 | },
198 | {
199 | "login": "nativedone",
200 | "name": "Adeildo Amorim",
201 | "avatar_url": "https://avatars2.githubusercontent.com/u/20998754?v=4",
202 | "profile": "https://github.com/nativedone",
203 | "contributions": [
204 | "doc"
205 | ]
206 | },
207 | {
208 | "login": "thegoodsheppard",
209 | "name": "Greg Sheppard",
210 | "avatar_url": "https://avatars1.githubusercontent.com/u/13774377?v=4",
211 | "profile": "https://github.com/thegoodsheppard",
212 | "contributions": [
213 | "doc"
214 | ]
215 | },
216 | {
217 | "login": "RafaelDavisH",
218 | "name": "Rafael D. Hernandez",
219 | "avatar_url": "https://avatars0.githubusercontent.com/u/6822714?v=4",
220 | "profile": "https://rafaeldavis.dev",
221 | "contributions": [
222 | "code"
223 | ]
224 | },
225 | {
226 | "login": "DallasCarraher",
227 | "name": "Dallas Carraher",
228 | "avatar_url": "https://avatars2.githubusercontent.com/u/4131693?v=4",
229 | "profile": "http://dallascarraher.dev",
230 | "contributions": [
231 | "doc"
232 | ]
233 | },
234 | {
235 | "login": "roni-castro",
236 | "name": "Roni Castro",
237 | "avatar_url": "https://avatars3.githubusercontent.com/u/24610813?v=4",
238 | "profile": "https://github.com/roni-castro",
239 | "contributions": [
240 | "test"
241 | ]
242 | },
243 | {
244 | "login": "thebrengun",
245 | "name": "Brennan",
246 | "avatar_url": "https://avatars2.githubusercontent.com/u/15270595?v=4",
247 | "profile": "https://github.com/thebrengun",
248 | "contributions": [
249 | "doc"
250 | ]
251 | },
252 | {
253 | "login": "DaleSeo",
254 | "name": "Dale Seo",
255 | "avatar_url": "https://avatars1.githubusercontent.com/u/5466341?v=4",
256 | "profile": "https://www.daleseo.com",
257 | "contributions": [
258 | "test"
259 | ]
260 | },
261 | {
262 | "login": "MichaelDeBoey",
263 | "name": "Michaël De Boey",
264 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
265 | "profile": "https://michaeldeboey.be",
266 | "contributions": [
267 | "code"
268 | ]
269 | },
270 | {
271 | "login": "bobbywarner",
272 | "name": "Bobby Warner",
273 | "avatar_url": "https://avatars0.githubusercontent.com/u/554961?v=4",
274 | "profile": "http://bobbywarner.com",
275 | "contributions": [
276 | "code"
277 | ]
278 | },
279 | {
280 | "login": "douglance",
281 | "name": "Doug Lance",
282 | "avatar_url": "https://avatars2.githubusercontent.com/u/4741454?v=4",
283 | "profile": "https://github.com/douglance",
284 | "contributions": [
285 | "doc"
286 | ]
287 | },
288 | {
289 | "login": "nekhaevskiy",
290 | "name": "Yury Nekhaevskiy",
291 | "avatar_url": "https://avatars0.githubusercontent.com/u/15379100?v=4",
292 | "profile": "https://github.com/nekhaevskiy",
293 | "contributions": [
294 | "doc"
295 | ]
296 | },
297 | {
298 | "login": "mansn",
299 | "name": "Måns Nilsson",
300 | "avatar_url": "https://avatars0.githubusercontent.com/u/4518977?v=4",
301 | "profile": "https://github.com/mansn",
302 | "contributions": [
303 | "doc"
304 | ]
305 | },
306 | {
307 | "login": "cwinters8",
308 | "name": "Clark Winters",
309 | "avatar_url": "https://avatars2.githubusercontent.com/u/40615752?v=4",
310 | "profile": "https://clarkwinters.com",
311 | "contributions": [
312 | "bug"
313 | ]
314 | },
315 | {
316 | "login": "omarhoumz",
317 | "name": "Omar Houmz",
318 | "avatar_url": "https://avatars2.githubusercontent.com/u/40954879?v=4",
319 | "profile": "https://omarhoumz.com/",
320 | "contributions": [
321 | "code"
322 | ]
323 | },
324 | {
325 | "login": "suddenlyGiovanni",
326 | "name": "Giovanni Ravalico",
327 | "avatar_url": "https://avatars2.githubusercontent.com/u/15946771?v=4",
328 | "profile": "https://suddenlyGiovanni.dev",
329 | "contributions": [
330 | "code",
331 | "ideas"
332 | ]
333 | },
334 | {
335 | "login": "Segebre",
336 | "name": "Juan Enrique Segebre Zaghmout",
337 | "avatar_url": "https://avatars3.githubusercontent.com/u/10774915?v=4",
338 | "profile": "https://github.com/Segebre",
339 | "contributions": [
340 | "code"
341 | ]
342 | },
343 | {
344 | "login": "Alferguson",
345 | "name": "John Alexander Ferguson",
346 | "avatar_url": "https://avatars.githubusercontent.com/u/30883573?v=4",
347 | "profile": "https://www.linkedin.com/in/johnalexanderferguson/",
348 | "contributions": [
349 | "test"
350 | ]
351 | },
352 | {
353 | "login": "trentschnee",
354 | "name": "Trent Schneweis",
355 | "avatar_url": "https://avatars.githubusercontent.com/u/10525549?v=4",
356 | "profile": "https://trentschneweis.com",
357 | "contributions": [
358 | "code"
359 | ]
360 | },
361 | {
362 | "login": "dlo",
363 | "name": "Dan Loewenherz",
364 | "avatar_url": "https://avatars.githubusercontent.com/u/38447?v=4",
365 | "profile": "https://github.com/lionheart",
366 | "contributions": [
367 | "code"
368 | ]
369 | },
370 | {
371 | "login": "shivaprabhu",
372 | "name": "Shivaprabhu",
373 | "avatar_url": "https://avatars.githubusercontent.com/u/40115160?v=4",
374 | "profile": "https://prabhuwali.me/",
375 | "contributions": [
376 | "doc"
377 | ]
378 | },
379 | {
380 | "login": "JacobParis",
381 | "name": "Jacob Paris",
382 | "avatar_url": "https://avatars.githubusercontent.com/u/5633704?v=4",
383 | "profile": "https://www.jacobparis.com/",
384 | "contributions": [
385 | "doc"
386 | ]
387 | }
388 | ],
389 | "contributorsPerLine": 7,
390 | "repoHost": "https://github.com",
391 | "skipCi": true
392 | }
393 |
--------------------------------------------------------------------------------
/src/exercise/06.md:
--------------------------------------------------------------------------------
1 | # useEffect: HTTP requests
2 |
3 | ## Recordings
4 | [Exercise](https://drive.google.com/file/d/1R52hWPnE7fGVYh5_da9sLmt3BnUxYbNA/view?usp=sharing)
5 | [Extra Credit 1](https://drive.google.com/file/d/1AceRf7kwxa30DPUBI_VtnXQ1cMRw6XxB/view?usp=sharing)
6 | [Extra Credit 2](https://drive.google.com/file/d/1G3xMpEdK2Dngiia4QJONXAZn-QRurF-3/view?usp=sharing)
7 | [Extra Credit 3](https://drive.google.com/file/d/1VZCBNDzZK3WRsmCG0lIIQBdUeeSCI3qq/view?usp=sharing)
8 | [Extra Credit 4](https://drive.google.com/file/d/1qjei4Z54OhwzhGbdei_68IeS_tivmiOC/view?usp=sharing)
9 | [Extra Credit 5](https://drive.google.com/file/d/1_N6SQO27DCNdFXzsrDrIacNQHGlrXaNn/view?usp=sharing)
10 | [Extra Credit 6](https://drive.google.com/file/d/1tSg6iO16AjSNL34QLTJyi0YCDxpyj2E3/view?usp=sharing)
11 | [Extra Credit 7](https://drive.google.com/file/d/1p-5oTT6A0RV5ObFKY097-WnyPgse4dge/view?usp=sharing)
12 |
13 | ## 📝 Your Notes
14 |
15 | Elaborate on your learnings here in `src/exercise/06.md`
16 |
17 | ## Background
18 |
19 | HTTP requests are another common side-effect that we need to do in applications.
20 | This is no different from the side-effects we need to apply to a rendered DOM or
21 | when interacting with browser APIs like localStorage. In all these cases, we do
22 | that within a `useEffect` hook callback. This hook allows us to ensure that
23 | whenever certain changes take place, we apply the side-effects based on those
24 | changes.
25 |
26 | One important thing to note about the `useEffect` hook is that you cannot return
27 | anything other than the cleanup function. This has interesting implications with
28 | regard to async/await syntax:
29 |
30 | ```javascript
31 | // this does not work, don't do this:
32 | React.useEffect(async () => {
33 | const result = await doSomeAsyncThing()
34 | // do something with the result
35 | })
36 | ```
37 |
38 | The reason this doesn't work is because when you make a function async, it
39 | automatically returns a promise (whether you're not returning anything at all,
40 | or explicitly returning a function). This is due to the semantics of async/await
41 | syntax. So if you want to use async/await, the best way to do that is like so:
42 |
43 | ```javascript
44 | React.useEffect(() => {
45 | async function effect() {
46 | const result = await doSomeAsyncThing()
47 | // do something with the result
48 | }
49 | effect()
50 | })
51 | ```
52 |
53 | This ensures that you don't return anything but a cleanup function.
54 |
55 | 🦉 I find that it's typically just easier to extract all the async code into a
56 | utility function which I call and then use the promise-based `.then` method
57 | instead of using async/await syntax:
58 |
59 | ```javascript
60 | React.useEffect(() => {
61 | doSomeAsyncThing().then(result => {
62 | // do something with the result
63 | })
64 | })
65 | ```
66 |
67 | But how you prefer to do this is totally up to you :)
68 |
69 | ## Exercise
70 |
71 | Production deploys:
72 |
73 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/06.js)
74 | - [Final](https://react-hooks.netlify.app/isolated/final/06.js)
75 |
76 | In this exercise, we'll be doing data fetching directly in a useEffect hook
77 | callback within our component.
78 |
79 | Here we have a form where users can enter the name of a pokemon and fetch data
80 | about that pokemon. Your job will be to create a component which makes that
81 | fetch request. When the user submits a pokemon name, our `PokemonInfo` component
82 | will get re-rendered with the `pokemonName`
83 |
84 | ## Extra Credit
85 |
86 | ### 1. 💯 handle errors
87 |
88 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-1.js)
89 |
90 | Unfortunately, sometimes things go wrong and we need to handle errors when they
91 | do so we can show the user useful information. Handle that error and render it
92 | out like so:
93 |
94 | ```jsx
95 |
96 | There was an error:
{error.message}
97 |
98 | ```
99 |
100 | You can make an error happen by typing an incorrect pokemon name into the input.
101 |
102 | One common question I get about this extra credit is how to handle promise
103 | errors. There are two ways to do it in this extra credit:
104 |
105 | ```javascript
106 | // option 1: using .catch
107 | fetchPokemon(pokemonName)
108 | .then(pokemon => setPokemon(pokemon))
109 | .catch(error => setError(error))
110 |
111 | // option 2: using the second argument to .then
112 | fetchPokemon(pokemonName).then(
113 | pokemon => setPokemon(pokemon),
114 | error => setError(error),
115 | )
116 | ```
117 |
118 | These are functionally equivalent for our purposes, but they are semantically
119 | different in general.
120 |
121 | Using `.catch` means that you'll handle an error in the `fetchPokemon` promise,
122 | but you'll _also_ handle an error in the `setPokemon(pokemon)` call as well.
123 | This is due to the semantics of how promises work.
124 |
125 | Using the second argument to `.then` means that you will catch an error that
126 | happens in `fetchPokemon` only. In this case, I knew that calling `setPokemon`
127 | would not throw an error (React handles errors and we have an API to catch those
128 | which we'll use later), so I decided to go with the second argument option.
129 |
130 | However, in this situation, it doesn't really make much of a difference. If you
131 | want to go with the safe option, then opt for `.catch`.
132 |
133 | ### 2. 💯 use a status
134 |
135 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-2.js)
136 |
137 | Our logic for what to show the user when is kind of convoluted and requires that
138 | we be really careful about which state we set and when.
139 |
140 | We could make things much simpler by having some state to set the explicit
141 | status of our component. Our component can be in the following "states":
142 |
143 | - `idle`: no request made yet
144 | - `pending`: request started
145 | - `resolved`: request successful
146 | - `rejected`: request failed
147 |
148 | Try to use a status state by setting it to these string values rather than
149 | relying on existing state or booleans.
150 |
151 | Learn more about this concept here:
152 | https://kentcdodds.com/blog/stop-using-isloading-booleans
153 |
154 | 💰 Warning: Make sure you call `setPokemon` before calling `setStatus`. We'll
155 | address that more in the next extra credit.
156 |
157 | ### 3. 💯 store the state in an object
158 |
159 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-3.js)
160 |
161 | You'll notice that we're calling a bunch of state updaters in a row. This is
162 | normally not a problem, but each call to our state updater can result in a
163 | re-render of our component. React normally batches these calls so you only get a
164 | single re-render, but it's unable to do this in an asynchronous callback (like
165 | our promise success and error handlers).
166 |
167 | So you might notice that if you do this:
168 |
169 | ```javascript
170 | setStatus('resolved')
171 | setPokemon(pokemon)
172 | ```
173 |
174 | You'll get an error indicating that you cannot read `image` of `null`. This is
175 | because the `setStatus` call results in a re-render that happens before the
176 | `setPokemon` happens.
177 |
178 | In the future, you'll learn about how `useReducer` can solve this problem really
179 | elegantly, but we can still accomplish this by storing our state as an object
180 | that has all the properties of state we're managing.
181 |
182 | See if you can figure out how to store all of your state in a single object with
183 | a single `React.useState` call so I can update my state like this:
184 |
185 | ```javascript
186 | setState({status: 'resolved', pokemon})
187 | ```
188 |
189 | ### 4. 💯 create an ErrorBoundary component
190 |
191 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-4.js)
192 |
193 | We've already solved the problem for errors in our request, we're only handling
194 | that one error. But there are a lot of different kinds of errors that can happen
195 | in our applications.
196 |
197 | No matter how hard you try, eventually your app code just isn’t going to behave
198 | the way you expect it to and you’ll need to handle those exceptions. If an error
199 | is thrown and unhandled, your application will be removed from the page, leaving
200 | the user with a blank screen... Kind of awkward...
201 |
202 | Luckily for us, there’s a simple way to handle errors in your application using
203 | a special kind of component called an
204 | [Error Boundary](https://reactjs.org/docs/error-boundaries.html). Unfortunately,
205 | there is currently no way to create an Error Boundary component with a function
206 | and you have to use a class component instead.
207 |
208 | In this extra credit, read up on ErrorBoundary components, and try to create one
209 | that handles this and any other error for the `PokemonInfo` component.
210 |
211 | 💰 to make your error boundary component handle errors from the `PokemonInfo`
212 | component, instead of rendering the error within the `PokemonInfo` component,
213 | you'll need to `throw error` right in the function so React can hand that to the
214 | error boundary. So `if (status === 'rejected') throw error`.
215 |
216 | ### 5. 💯 re-mount the error boundary
217 |
218 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-5.js)
219 |
220 | You might notice that with the changes we've added, we now cannot recover from
221 | an error. For example:
222 |
223 | 1. Type an incorrect pokemon
224 | 2. Notice the error
225 | 3. Type a correct pokemon
226 | 4. Notice it doesn't show that new pokemon's information
227 |
228 | The reason this is happening is because the `error` that's stored in the
229 | internal state of the `ErrorBoundary` component isn't getting reset, so it's not
230 | rendering the `children` we're passing to it.
231 |
232 | So what we need to do is reset the ErrorBoundary's `error` state to `null` so it
233 | will re-render. But how do we access the internal state of our `ErrorBoundary`
234 | to reset it? Well, there are a few ways we could do this by modifying the
235 | `ErrorBoundary`, but one thing you can do when you want to _reset_ the state of
236 | a component, is by providing it a `key` prop which can be used to unmount and
237 | re-mount a component.
238 |
239 | The `key` you can use? Try the `pokemonName`!
240 |
241 | ### 6. 💯 use react-error-boundary
242 |
243 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-6.js)
244 |
245 | As cool as our own `ErrorBoundary` is, I'd rather not have to maintain it in the
246 | long-term. Luckily for us, there's an npm package we can use instead and it's
247 | already installed into this project. It's called
248 | [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary).
249 |
250 | Go ahead and give that a look and swap out our own `ErrorBoundary` for the one
251 | from `react-error-boundary`.
252 |
253 | ### 7. 💯 reset the error boundary
254 |
255 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-7.js)
256 |
257 | You may have noticed a problem with the way we're resetting the internal state
258 | of the `ErrorBoundary` using the `key`. Unfortunately, we're not only
259 | re-mounting the `ErrorBoundary`, we're also re-mounting the `PokemonInfo` which
260 | results in a flash of the initial "Submit a pokemon" state whenever we change
261 | our pokemon.
262 |
263 | So let's backtrack on that and instead we'll use `react-error-boundary`'s
264 | `resetErrorBoundary` function (which will be passed to our `ErrorFallback`
265 | component) to reset the state of the `ErrorBoundary` when the user clicks a "try
266 | again" button.
267 |
268 | > 💰 feel free to open up the finished version by clicking the link in the app
269 | > so you can get an idea of how this is supposed to work.
270 |
271 | Once you have this button wired up, we need to react to this reset of the
272 | `ErrorBoundary`'s state by resetting our own state so we don't wind up
273 | triggering the error again. To do this we can use the `onReset` prop of the
274 | `ErrorBoundary`. In that function we can simply `setPokemonName` to an empty
275 | string.
276 |
277 | ### 8. 💯 use resetKeys
278 |
279 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-8.js)
280 |
281 | Unfortunately now the user can't simply select a new pokemon and continue with
282 | their day. They have to first click "Try again" and then select their new
283 | pokemon. I think it would be cooler if they can just submit a new `pokemonName`
284 | and the `ErrorBoundary` would reset itself automatically.
285 |
286 | Luckily for us `react-error-boundary` supports this with the `resetKeys` prop.
287 | You pass an array of values to `resetKeys` and if the `ErrorBoundary` is in an
288 | error state and any of those values change, it will reset the error boundary.
289 |
290 | 💰 Your `resetKeys` prop should be: `[pokemonName]`
291 |
292 | ## 🦉 Feedback
293 |
294 | Fill out
295 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=06%3A%20useEffect%3A%20HTTP%20requests&em=).
296 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
3 |
4 | Learn the ins and outs of React Hooks.
5 |
6 |
7 | I will take you on a deep dive into
8 | React Hooks, and show you what you need to know to start using them in your
9 | applications right away.
10 |
19 |
20 |
21 |
22 |
23 | [![Build Status][build-badge]][build]
24 | [![All Contributors][all-contributors-badge]](#contributors-)
25 | [![GPL 3.0 License][license-badge]][license]
26 | [![Code of Conduct][coc-badge]][coc]
27 |
28 |
29 | ## Prerequisites
30 |
31 | - Watch my talk
32 | [Why React Hooks](https://www.youtube.com/watch?v=zWsZcBiwgVE&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
33 | (35 minutes)
34 |
35 | ## System Requirements
36 |
37 | - [git][git] v2.13 or greater
38 | - [NodeJS][node] `12 || 14 || 15 || 16`
39 | - [npm][npm] v6 or greater
40 |
41 | All of these must be available in your `PATH`. To verify things are set up
42 | properly, you can run this:
43 |
44 | ```shell
45 | git --version
46 | node --version
47 | npm --version
48 | ```
49 |
50 | If you have trouble with any of these, learn more about the PATH environment
51 | variable and how to fix it here for [windows][win-path] or
52 | [mac/linux][mac-path].
53 |
54 | ## Setup
55 |
56 | > If you want to commit and push your work as you go, you'll want to
57 | > [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo)
58 | > first and then clone your fork rather than this repo directly.
59 |
60 | After you've made sure to have the correct things (and versions) installed, you
61 | should be able to just run a few commands to get set up:
62 |
63 | ```
64 | git clone https://github.com/kentcdodds/react-hooks.git
65 | cd react-hooks
66 | node setup
67 | ```
68 |
69 | This may take a few minutes. **It will ask you for your email.** This is
70 | optional and just automatically adds your email to the links in the project to
71 | make filling out some forms easier.
72 |
73 | If you get any errors, please read through them and see if you can find out what
74 | the problem is. If you can't work it out on your own then please [file an
75 | issue][issue] and provide _all_ the output from the commands you ran (even if
76 | it's a lot).
77 |
78 | If you can't get the setup script to work, then just make sure you have the
79 | right versions of the requirements listed above, and run the following commands:
80 |
81 | ```
82 | npm install
83 | npm run validate
84 | ```
85 |
86 | If you are still unable to fix issues and you know how to use Docker 🐳 you can
87 | setup the project with the following command:
88 |
89 | ```
90 | docker-compose up
91 | ```
92 |
93 | It's recommended you run everything locally in the same environment you work in
94 | every day, but if you're having issues getting things set up, you can also set
95 | this up using [GitHub Codespaces](https://github.com/features/codespaces)
96 | ([video demo](https://www.youtube.com/watch?v=gCoVJm3hGk4)) or
97 | [Codesandbox](https://codesandbox.io/s/github/kentcdodds/react-hooks).
98 |
99 | ## Running the app
100 |
101 | To get the app up and running (and really see if it worked), run:
102 |
103 | ```shell
104 | npm start
105 | ```
106 |
107 | This should start up your browser. If you're familiar, this is a standard
108 | [react-scripts](https://create-react-app.dev/) application.
109 |
110 | You can also open
111 | [the deployment of the app on Netlify](https://react-hooks.netlify.app/).
112 |
113 | ## Running the tests
114 |
115 | ```shell
116 | npm test
117 | ```
118 |
119 | This will start [Jest](https://jestjs.io/) in watch mode. Read the output and
120 | play around with it. The tests are there to help you reach the final version,
121 | however _sometimes_ you can accomplish the task and the tests still fail if you
122 | implement things differently than I do in my solution, so don't look to them as
123 | a complete authority.
124 |
125 | ### Exercises
126 |
127 | - `src/exercise/00.md`: Background, Exercise Instructions, Extra Credit
128 | - `src/exercise/00.js`: Exercise with Emoji helpers
129 | - `src/__tests__/00.js`: Tests
130 | - `src/final/00.js`: Final version
131 | - `src/final/00.extra-0.js`: Final version of extra credit
132 |
133 | The purpose of the exercise is **not** for you to work through all the material.
134 | It's intended to get your brain thinking about the right questions to ask me as
135 | _I_ walk through the material.
136 |
137 | ### Helpful Emoji 🐨 💰 💯 📝 🦉 📜 💣 💪 🏁 👨💼 🚨
138 |
139 | Each exercise has comments in it to help you get through the exercise. These fun
140 | emoji characters are here to help you.
141 |
142 | - **Kody the Koala** 🐨 will tell you when there's something specific you should
143 | do version
144 | - **Marty the Money Bag** 💰 will give you specific tips (and sometimes code)
145 | along the way
146 | - **Hannah the Hundred** 💯 will give you extra challenges you can do if you
147 | finish the exercises early.
148 | - **Nancy the Notepad** 📝 will encourage you to take notes on what you're
149 | learning
150 | - **Olivia the Owl** 🦉 will give you useful tidbits/best practice notes and a
151 | link for elaboration and feedback.
152 | - **Dominic the Document** 📜 will give you links to useful documentation
153 | - **Berry the Bomb** 💣 will be hanging around anywhere you need to blow stuff
154 | up (delete code)
155 | - **Matthew the Muscle** 💪 will indicate that you're working with an exercise
156 | - **Chuck the Checkered Flag** 🏁 will indicate that you're working with a final
157 | - **Peter the Product Manager** 👨💼 helps us know what our users want
158 | - **Alfred the Alert** 🚨 will occasionally show up in the test failures with
159 | potential explanations for why the tests are failing.
160 |
161 | ## Contributors
162 |
163 | Thanks goes to these wonderful people
164 | ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
165 |
166 |
167 |
168 |
169 |