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 | [![Gitpod ready-to-code][gitpod-badge]](https://gitpod.io/#https://github.com/kentcdodds/react-hooks)
28 |
29 |
30 | ## Prerequisites
31 |
32 | - Watch my talk
33 | [Why React Hooks](https://www.youtube.com/watch?v=zWsZcBiwgVE&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
34 | (35 minutes)
35 |
36 | > NOTE: The EpicReact.dev videos were recorded with React version ^16.13 and all
37 | > material in this repo has been updated to React version ^18. Differences are
38 | > minor and any relevant differences are noted in the instructions.
39 |
40 | ## Quick start
41 |
42 | It's recommended you run everything in the same environment you work in every
43 | day, but if you don't want to set up the repository locally, you can get started
44 | in one click with [Gitpod](https://gitpod.io),
45 | [CodeSandbox](https://codesandbox.io/s/github/kentcdodds/react-hooks), or by
46 | following the [video demo](https://www.youtube.com/watch?v=gCoVJm3hGk4)
47 | instructions for [GitHub Codespaces](https://github.com/features/codespaces).
48 |
49 | [](https://gitpod.io/#https://github.com/kentcdodds/react-hooks)
50 |
51 | For a local development environment, follow the instructions below
52 |
53 | ## System Requirements
54 |
55 | - [git][git] v2.13 or greater
56 | - [NodeJS][node] `>=16`
57 | - [npm][npm] v8.16.0 or greater
58 |
59 | All of these must be available in your `PATH`. To verify things are set up
60 | properly, you can run this:
61 |
62 | ```shell
63 | git --version
64 | node --version
65 | npm --version
66 | ```
67 |
68 | If you have trouble with any of these, learn more about the PATH environment
69 | variable and how to fix it here for [windows][win-path] or
70 | [mac/linux][mac-path].
71 |
72 | ## Setup
73 |
74 | > If you want to commit and push your work as you go, you'll want to
75 | > [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo)
76 | > first and then clone your fork rather than this repo directly.
77 |
78 | After you've made sure to have the correct things (and versions) installed, you
79 | should be able to just run a few commands to get set up:
80 |
81 | ```shell
82 | git clone https://github.com/kentcdodds/react-hooks.git
83 | cd react-hooks
84 | node setup
85 | ```
86 |
87 | This may take a few minutes. **It will ask you for your email.** This is
88 | optional and just automatically adds your email to the links in the project to
89 | make filling out some forms easier.
90 |
91 | If you get any errors, please read through them and see if you can find out what
92 | the problem is. If you can't work it out on your own then please [file an
93 | issue][issue] and provide _all_ the output from the commands you ran (even if
94 | it's a lot).
95 |
96 | If you can't get the setup script to work, then just make sure you have the
97 | right versions of the requirements listed above, and run the following commands:
98 |
99 | ```shell
100 | npm install
101 | npm run validate
102 | ```
103 |
104 | If you are still unable to fix issues and you know how to use Docker 🐳 you can
105 | setup the project with the following command:
106 |
107 | ```shell
108 | docker-compose up
109 | ```
110 |
111 | ## Running the app
112 |
113 | To get the app up and running (and really see if it worked), run:
114 |
115 | ```shell
116 | npm start
117 | ```
118 |
119 | This should start up your browser. If you're familiar, this is a standard
120 | [react-scripts](https://create-react-app.dev/) application.
121 |
122 | You can also open
123 | [the deployment of the app on Netlify](https://react-hooks.netlify.app/).
124 |
125 | ## Running the tests
126 |
127 | ```shell
128 | npm test
129 | ```
130 |
131 | This will start [Jest](https://jestjs.io/) in watch mode. Read the output and
132 | play around with it. The tests are there to help you reach the final version,
133 | however _sometimes_ you can accomplish the task and the tests still fail if you
134 | implement things differently than I do in my solution, so don't look to them as
135 | a complete authority.
136 |
137 | ### Exercises
138 |
139 | - `src/exercise/00.md`: Background, Exercise Instructions, Extra Credit
140 | - `src/exercise/00.js`: Exercise with Emoji helpers
141 | - `src/__tests__/00.js`: Tests
142 | - `src/final/00.js`: Final version
143 | - `src/final/00.extra-0.js`: Final version of extra credit
144 |
145 | The purpose of the exercise is **not** for you to work through all the material.
146 | It's intended to get your brain thinking about the right questions to ask me as
147 | _I_ walk through the material.
148 |
149 | ### Helpful Emoji 🐨 💰 💯 📝 🦉 📜 💣 💪 🏁 👨💼 🚨
150 |
151 | Each exercise has comments in it to help you get through the exercise. These fun
152 | emoji characters are here to help you.
153 |
154 | - **Kody the Koala** 🐨 will tell you when there's something specific you should
155 | do
156 | - **Marty the Money Bag** 💰 will give you specific tips (and sometimes code)
157 | along the way
158 | - **Hannah the Hundred** 💯 will give you extra challenges you can do if you
159 | finish the exercises early.
160 | - **Nancy the Notepad** 📝 will encourage you to take notes on what you're
161 | learning
162 | - **Olivia the Owl** 🦉 will give you useful tidbits/best practice notes and a
163 | link for elaboration and feedback.
164 | - **Dominic the Document** 📜 will give you links to useful documentation
165 | - **Berry the Bomb** 💣 will be hanging around anywhere you need to blow stuff
166 | up (delete code)
167 | - **Matthew the Muscle** 💪 will indicate that you're working with an exercise
168 | - **Chuck the Checkered Flag** 🏁 will indicate that you're working with a final
169 | - **Peter the Product Manager** 👨💼 helps us know what our users want
170 | - **Alfred the Alert** 🚨 will occasionally show up in the test failures with
171 | potential explanations for why the tests are failing.
172 |
173 | ## Contributors
174 |
175 | Thanks goes to these wonderful people
176 | ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
177 |
178 |
179 |
180 |
181 |
28 |
31 |
35 | {name ? Hello {name} : 'Please type your name'}
36 |
37 | )
38 | }
39 |
40 | export default Greeting
41 |
--------------------------------------------------------------------------------
/src/exercise/01.js:
--------------------------------------------------------------------------------
1 | // useState: greeting
2 | // http://localhost:3000/isolated/exercise/01.js
3 |
4 | import * as React from 'react'
5 |
6 | function Greeting() {
7 | // 💣 delete this variable declaration and replace it with a React.useState call
8 | const name = ''
9 |
10 | function handleChange(event) {
11 | // 🐨 update the name here based on event.target.value
12 | }
13 |
14 | return (
15 |
16 |
20 | {name ? Hello {name} : 'Please type your name'}
21 |
22 | )
23 | }
24 |
25 | function App() {
26 | return
27 | }
28 |
29 | export default App
30 |
--------------------------------------------------------------------------------
/src/exercise/01.md:
--------------------------------------------------------------------------------
1 | # useState: greeting
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/01.md`
6 |
7 | ## Background
8 |
9 | Normally an interactive application will need to hold state somewhere. In React,
10 | you use special functions called "hooks" to do this. Common built-in hooks
11 | include:
12 |
13 | - `React.useState`
14 | - `React.useEffect`
15 | - `React.useContext`
16 | - `React.useRef`
17 | - `React.useReducer`
18 |
19 | Each of these is a special function that you can call inside your custom React
20 | component function to store data (like state) or perform actions (or
21 | side-effects). There are a few more built-in hooks that have special use cases,
22 | but the ones above are what you'll be using most of the time.
23 |
24 | Each of the hooks has a unique API. Some return a value (like `React.useRef` and
25 | `React.useContext`), others return a pair of values (like `React.useState` and
26 | `React.useReducer`), and others return nothing at all (like `React.useEffect`).
27 |
28 | Here's an example of a component that uses the `useState` hook and an onClick
29 | event handler to update that state:
30 |
31 | ```jsx
32 | function Counter() {
33 | const [count, setCount] = React.useState(0)
34 | const increment = () => setCount(count + 1)
35 | return
36 | }
37 | ```
38 |
39 | `React.useState` is a function that accepts a single argument. That argument is
40 | the initial state for the instance of the component. In our case, the state will
41 | start as `0`.
42 |
43 | `React.useState` returns a pair of values. It does this by returning an array
44 | with two elements (and we use destructuring syntax to assign each of those
45 | values to distinct variables). The first of the pair is the state value and the
46 | second is a function we can call to update the state. We can name these
47 | variables whatever we want. Common convention is to choose a name for the state
48 | variable, then prefix `set` in front of that for the updater function.
49 |
50 | State can be defined as: data that changes over time. So how does this work over
51 | time? When the button is clicked, our `increment` function will be called at
52 | which time we update the `count` by calling `setCount`.
53 |
54 | When we call `setCount`, that tells React to re-render our component. When it
55 | does this, the entire `Counter` function is re-run, so when `React.useState` is
56 | called this time, the value we get back is the value that we called `setCount`
57 | with. And it continues like that until `Counter` is unmounted (removed from the
58 | application), or the user closes the application.
59 |
60 | ## Exercise
61 |
62 | Production deploys:
63 |
64 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/01.js)
65 | - [Final](https://react-hooks.netlify.app/isolated/final/01.js)
66 |
67 | In this exercise we have a form where you can type in your name and it will give
68 | you a greeting as you type. Fill out the `Greeting` component so that it manages
69 | the state of the name and shows the greeting as the name is changed.
70 |
71 | ## Extra Credit
72 |
73 | ### 1. 💯 accept an initialName
74 |
75 | [Production deploy](https://react-hooks.netlify.app/isolated/final/01.extra-1.js)
76 |
77 | Make the `Greeting` accept a prop called `initialName` and initialize the `name`
78 | state to that value.
79 |
80 | ## 🦉 Feedback
81 |
82 | Fill out
83 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=01%3A%20useState%3A%20greeting&em=).
84 |
--------------------------------------------------------------------------------
/src/exercise/02.js:
--------------------------------------------------------------------------------
1 | // useEffect: persistent state
2 | // http://localhost:3000/isolated/exercise/02.js
3 |
4 | import * as React from 'react'
5 |
6 | function Greeting({initialName = ''}) {
7 | // 🐨 initialize the state to the value from localStorage
8 | // 💰 window.localStorage.getItem('name') ?? initialName
9 | const [name, setName] = React.useState(initialName)
10 |
11 | // 🐨 Here's where you'll use `React.useEffect`.
12 | // The callback should set the `name` in localStorage.
13 | // 💰 window.localStorage.setItem('name', name)
14 |
15 | function handleChange(event) {
16 | setName(event.target.value)
17 | }
18 | return (
19 |
20 |
24 | {name ? Hello {name} : 'Please type your name'}
25 |
26 | )
27 | }
28 |
29 | function App() {
30 | return
31 | }
32 |
33 | export default App
34 |
--------------------------------------------------------------------------------
/src/exercise/02.md:
--------------------------------------------------------------------------------
1 | # useEffect: persistent state
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/02.md`
6 |
7 | ## Background
8 |
9 | `React.useEffect` is a built-in hook that allows you to run some custom code
10 | after React renders (and re-renders) your component to the DOM. It accepts a
11 | callback function which React will call after the DOM has been updated:
12 |
13 | ```javascript
14 | React.useEffect(() => {
15 | // your side-effect code here.
16 | // this is where you can make HTTP requests or interact with browser APIs.
17 | })
18 | ```
19 |
20 | Feel free to take a look at `src/examples/hook-flow.png` if you're interested in
21 | the timing of when your functions are run. This will make more sense after
22 | finishing the exercises/extra credit/instruction.
23 |
24 | ## Exercise
25 |
26 | Production deploys:
27 |
28 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/02.js)
29 | - [Final](https://react-hooks.netlify.app/isolated/final/02.js)
30 |
31 | In this exercise, we're going to enhance our `` component to get its
32 | initial state value from localStorage (if available) and keep localStorage
33 | updated as the `name` is updated.
34 |
35 | ## Extra Credit
36 |
37 | ### 1. 💯 lazy state initialization
38 |
39 | [Production deploy](https://react-hooks.netlify.app/isolated/final/02.extra-1.js)
40 |
41 | Right now, every time our component function is run, our function reads from
42 | localStorage. This is problematic because it could be a performance bottleneck
43 | (reading from localStorage can be slow). And what's more we only actually need
44 | to know the value from localStorage the first time this component is rendered!
45 | So the additional reads are wasted effort.
46 |
47 | To avoid this problem, React's useState hook allows you to pass a function
48 | instead of the actual value, and then it will only call that function to get the
49 | state value when the component is rendered the first time. So you can go from
50 | this: `React.useState(someExpensiveComputation())` To this:
51 | `React.useState(() => someExpensiveComputation())`
52 |
53 | And the `someExpensiveComputation` function will only be called when it's
54 | needed!
55 |
56 | Make the `React.useState` call use lazy initialization to avoid a performance
57 | bottleneck of reading into localStorage on every render.
58 |
59 | > Learn more about
60 | > [lazy state initialization](https://kentcdodds.com/blog/use-state-lazy-initialization-and-function-updates)
61 |
62 | ### 2. 💯 effect dependencies
63 |
64 | [Production deploy](https://react-hooks.netlify.app/isolated/final/02.extra-2.js)
65 |
66 | The callback we're passing to `React.useEffect` is called after _every_ render
67 | of our component (including re-renders). This is exactly what we want because we
68 | want to make sure that the `name` is saved into localStorage whenever it
69 | changes, but there are various reasons a component can be re-rendered (for
70 | example, when a parent component in the application tree gets re-rendered).
71 |
72 | Really, we _only_ want localStorage to get updated when the `name` state
73 | actually changes. It doesn't need to re-run any other time. Luckily for us,
74 | `React.useEffect` allows you to pass a second argument called the "dependency
75 | array" which signals to React that your effect callback function should be
76 | called when (and only when) those dependencies change. So we can use this to
77 | avoid doing unnecessary work!
78 |
79 | Add a dependencies array for `React.useEffect` to avoid the callback being
80 | called too frequently.
81 |
82 | ### 3. 💯 custom hook
83 |
84 | [Production deploy](https://react-hooks.netlify.app/isolated/final/02.extra-3.js)
85 |
86 | The best part of hooks is that if you find a bit of logic inside your component
87 | function that you think would be useful elsewhere, you can put that in another
88 | function and call it from the components that need it (just like regular
89 | JavaScript). These functions you create are called "custom hooks".
90 |
91 | Create a custom hook called `useLocalStorageState` for reusability of all this
92 | logic.
93 |
94 | ### 4. 💯 flexible localStorage hook
95 |
96 | [Production deploy](https://react-hooks.netlify.app/isolated/final/02.extra-4.js)
97 |
98 | Take your custom `useLocalStorageState` hook and make it generic enough to
99 | support any data type (remember, you have to serialize objects to strings... use
100 | `JSON.stringify` and `JSON.parse`). Go wild with this!
101 |
102 | ## Notes
103 |
104 | If you'd like to learn more about when different hooks are called and the order
105 | in which they're called, then open up `src/examples/hook-flow.png` and
106 | `src/examples/hook-flow.js`. Play around with that a bit and hopefully that will
107 | help solidify this for you. Note that understanding this isn't absolutely
108 | necessary for you to understand hooks, but it _will_ help you in some situations
109 | so it's useful to understand.
110 |
111 | > PLEASE NOTE: there was a subtle change in the order of cleanup functions
112 | > getting called in React 17:
113 | >
114 |
115 | ## 🦉 Feedback
116 |
117 | Fill out
118 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=02%3A%20useEffect%3A%20persistent%20state&em=).
119 |
--------------------------------------------------------------------------------
/src/exercise/03.js:
--------------------------------------------------------------------------------
1 | // Lifting state
2 | // http://localhost:3000/isolated/exercise/03.js
3 |
4 | import * as React from 'react'
5 |
6 | function Name({name, onNameChange}) {
7 | return (
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | // 🐨 accept `animal` and `onAnimalChange` props to this component
16 | function FavoriteAnimal() {
17 | // 💣 delete this, it's now managed by the App
18 | const [animal, setAnimal] = React.useState('')
19 | return (
20 |
{`Hey ${name}, your favorite animal is: ${animal}!`}
34 | // }
35 |
36 | // 💣 remove this component in favor of the new one
37 | function Display({name}) {
38 | return
{`Hey ${name}, you are great!`}
39 | }
40 |
41 | function App() {
42 | // 🐨 add a useState for the animal
43 | const [name, setName] = React.useState('')
44 | return (
45 |
52 | )
53 | }
54 |
55 | export default App
56 |
--------------------------------------------------------------------------------
/src/exercise/03.md:
--------------------------------------------------------------------------------
1 | # Lifting state
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/03.md`
6 |
7 | ## Background
8 |
9 | A common question from React beginners is how to share state between two sibling
10 | components. The answer is to
11 | ["lift the state"](https://react.dev/learn/sharing-state-between-components)
12 | which basically amounts to finding the lowest common parent shared between the
13 | two components and placing the state management there, and then passing the
14 | state and a mechanism for updating that state down into the components that need
15 | it.
16 |
17 | ## Exercise
18 |
19 | Production deploys:
20 |
21 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/03.js)
22 | - [Final](https://react-hooks.netlify.app/isolated/final/03.js)
23 |
24 | 👨💼 Peter told us we've got a new feature request for the `Display` component. He
25 | wants us to display the `animal` the user selects. But that state is managed in
26 | a "sibling" component, so we have to move that management to the lowest common
27 | parent (`App`) and then pass it down.
28 |
29 | ## Extra Credit
30 |
31 | ### 1. 💯 colocating state
32 |
33 | [Production deploy](https://react-hooks.netlify.app/isolated/final/03.extra-1.js)
34 |
35 | As a community we’re pretty good at lifting state. It becomes natural over time.
36 | One thing that we typically have trouble remembering to do is to push state back
37 | down (or
38 | [colocate state](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster)).
39 |
40 | 👨💼 Peter told us that now users only want the animal displayed instead of the
41 | name:
42 |
43 | ```javascript
44 | function Display({animal}) {
45 | return
{`Your favorite animal is: ${animal}!`}
46 | }
47 | ```
48 |
49 | You'll notice that just updating the `Display` component to this works fine, but
50 | for the extra credit, go through the process of moving state to the components
51 | that need it. You know what you just did for the `Animal` component? You need to
52 | do the opposite thing for the `Name` component.
53 |
54 | ## 🦉 Feedback
55 |
56 | Fill out
57 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=03%3A%20Lifting%20state&em=).
58 |
--------------------------------------------------------------------------------
/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 |
82 | )
83 | }
84 |
85 | // eslint-disable-next-line no-unused-vars
86 | function calculateStatus(winner, squares, nextValue) {
87 | return winner
88 | ? `Winner: ${winner}`
89 | : squares.every(Boolean)
90 | ? `Scratch: Cat's game`
91 | : `Next player: ${nextValue}`
92 | }
93 |
94 | // eslint-disable-next-line no-unused-vars
95 | function calculateNextValue(squares) {
96 | return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
97 | }
98 |
99 | // eslint-disable-next-line no-unused-vars
100 | function calculateWinner(squares) {
101 | const lines = [
102 | [0, 1, 2],
103 | [3, 4, 5],
104 | [6, 7, 8],
105 | [0, 3, 6],
106 | [1, 4, 7],
107 | [2, 5, 8],
108 | [0, 4, 8],
109 | [2, 4, 6],
110 | ]
111 | for (let i = 0; i < lines.length; i++) {
112 | const [a, b, c] = lines[i]
113 | if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
114 | return squares[a]
115 | }
116 | }
117 | return null
118 | }
119 |
120 | function App() {
121 | return
122 | }
123 |
124 | export default App
125 |
--------------------------------------------------------------------------------
/src/exercise/04.md:
--------------------------------------------------------------------------------
1 | # useState: tic tac toe
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/04.md`
6 |
7 | ## Background
8 |
9 | A `name` is one thing, but a real UI is a bit different. Often you need more
10 | than one element of state in your component, so you'll call `React.useState`
11 | more than once. Please note that each call to `React.useState` in a given
12 | component will give you a unique state and updater function.
13 |
14 | ## Exercise
15 |
16 | Production deploys:
17 |
18 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/04.js)
19 | - [Final](https://react-hooks.netlify.app/isolated/final/04.js)
20 |
21 | We're going to build tic-tac-toe (with localStorage support)! If you've gone
22 | through React's official tutorial, this was lifted from that.
23 |
24 | You're going to need some managed state and some derived state:
25 |
26 | - **Managed State:** State that you need to explicitly manage
27 | - **Derived State:** State that you can calculate based on other state
28 |
29 | `squares` is the managed state and it's the state of the board in a
30 | single-dimensional array:
31 |
32 | ```
33 | [
34 | 'X', 'O', 'X',
35 | 'X', 'O', 'O',
36 | 'X', 'X', 'O'
37 | ]
38 | ```
39 |
40 | This will start out as an empty array because it's the start of the game.
41 |
42 | `nextValue` will be either the string `X` or `O` and is derived state which you
43 | can determine based on the value of `squares`. We can determine whose turn it is
44 | based on how many "X" and "O" squares there are. We've written this out for you
45 | in a `calculateNextValue` function at the bottom of the file.
46 |
47 | `winner` will be either the string `X` or `O` and is derived state which can
48 | also be determined based on the value of `squares` and we've provided a
49 | `calculateWinner` function you can use to get that value.
50 |
51 | 📜 Read more about derived state in
52 | [Don't Sync State. Derive It!](https://kentcdodds.com/blog/dont-sync-state-derive-it)
53 |
54 | ### Alternate
55 |
56 | If you'd prefer to practice refactoring a class that does this to a hook, then
57 | you can open `src/exercise/04-classes.js` and open that on
58 | [an isolated page](http://localhost:3000/isolated/exercise/04-classes.js) to
59 | practice that.
60 |
61 | ## Extra Credit
62 |
63 | ### 1. 💯 preserve state in localStorage
64 |
65 | [Production deploy](https://react-hooks.netlify.app/isolated/final/04.extra-1.js)
66 |
67 | 👨💼 Our customers want to be able to pause a game, close the tab, and then resume
68 | the game later. Can you store the game's state in `localStorage`?
69 |
70 | ### 2. 💯 useLocalStorageState
71 |
72 | [Production deploy](https://react-hooks.netlify.app/isolated/final/04.extra-2.js)
73 |
74 | It's cool that we can get localStorage support with a simple `useEffect`, but
75 | it'd be even cooler to use the `useLocalStorageState` hook that's already
76 | written for us in `src/utils.js`!
77 |
78 | Refactor your code to use that custom hook instead. (This should be a pretty
79 | quick extra credit).
80 |
81 | ### 3. 💯 add game history feature
82 |
83 | [Production deploy](https://react-hooks.netlify.app/isolated/final/04.extra-3.js)
84 |
85 | Open [http://localhost:3000/isolated/final/04.extra-3.js](http://localhost:3000/isolated/final/04.extra-3.js) and see that the extra
86 | version supports keeping a history of the game and allows you to go backward and
87 | forward in time. See if you can implement that!
88 |
89 | NOTE: This extra credit is one of the harder extra credits. Don't worry if you
90 | struggle on it!
91 |
92 | 💰 Tip, in the final example, we store the history of squares in an array of
93 | arrays. `[[/* step 0 squares */], [/* step 1 squares */], ...etc]`, so we have
94 | two states: `history` and `currentStep`.
95 |
96 | 💰 Tip, in the final example, we move the state management from the `Board`
97 | component to the `Game` component and that helps a bit. Here's what the JSX
98 | returned from the `Game` component is in the final version:
99 |
100 | ```javascript
101 | return (
102 |
103 |
104 |
105 |
108 |
109 |
110 |
{status}
111 | {moves}
112 |
113 |
114 | )
115 | ```
116 |
117 | ## 🦉 Feedback
118 |
119 | Fill out
120 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=04%3A%20useState%3A%20tic%20tac%20toe&em=).
121 |
--------------------------------------------------------------------------------
/src/exercise/05-classes.js:
--------------------------------------------------------------------------------
1 | // useRef and useEffect: DOM interaction
2 | // 💯 (alternate) migrate from classes
3 | // http://localhost:3000/isolated/exercise/05-classes.js
4 |
5 | import * as React from 'react'
6 | import VanillaTilt from 'vanilla-tilt'
7 |
8 | // If you'd rather practice refactoring a class component to a function
9 | // component with hooks, then go ahead and do this exercise.
10 |
11 | class Tilt extends React.Component {
12 | tiltRef = React.createRef()
13 | componentDidMount() {
14 | const tiltNode = this.tiltRef.current
15 | const vanillaTiltOptions = {
16 | max: 25,
17 | speed: 400,
18 | glare: true,
19 | 'max-glare': 0.5,
20 | }
21 | VanillaTilt.init(tiltNode, vanillaTiltOptions)
22 | }
23 | componentWillUnmount() {
24 | this.tiltRef.current.vanillaTilt.destroy()
25 | }
26 | render() {
27 | return (
28 |
38 |
39 | )
40 | }
41 |
42 | export default App
43 |
--------------------------------------------------------------------------------
/src/exercise/05.js:
--------------------------------------------------------------------------------
1 | // useRef and useEffect: DOM interaction
2 | // http://localhost:3000/isolated/exercise/05.js
3 |
4 | import * as React from 'react'
5 | // eslint-disable-next-line no-unused-vars
6 | import VanillaTilt from 'vanilla-tilt'
7 |
8 | function Tilt({children}) {
9 | // 🐨 create a ref here with React.useRef()
10 |
11 | // 🐨 add a `React.useEffect` callback here and use VanillaTilt to make your
12 | // div look fancy.
13 | // 💰 like this:
14 | // const tiltNode = tiltRef.current
15 | // VanillaTilt.init(tiltNode, {
16 | // max: 25,
17 | // speed: 400,
18 | // glare: true,
19 | // 'max-glare': 0.5,
20 | // })
21 | //
22 | // 💰 Don't forget to return a cleanup function. VanillaTilt.init will add an
23 | // object to your DOM node to cleanup:
24 | // `return () => tiltNode.vanillaTilt.destroy()`
25 | //
26 | // 💰 Don't forget to specify your effect's dependencies array! In our case
27 | // we know that the tilt node will never change, so make it `[]`. Ask me about
28 | // this for a more in depth explanation.
29 |
30 | // 🐨 add the `ref` prop to the `tilt-root` div here:
31 | return (
32 |
42 |
43 | )
44 | }
45 |
46 | export default App
47 |
--------------------------------------------------------------------------------
/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 function component. In fact,
13 | DOM nodes aren't created at all until the `ReactDOM.render` method is called.
14 | Your function component 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/exercise/06.js:
--------------------------------------------------------------------------------
1 | // useEffect: HTTP requests
2 | // http://localhost:3000/isolated/exercise/06.js
3 |
4 | import * as React from 'react'
5 | // 🐨 you'll want the following additional things from '../pokemon':
6 | // fetchPokemon: the function we call to get the pokemon info
7 | // PokemonInfoFallback: the thing we show while we're loading the pokemon info
8 | // PokemonDataView: the stuff we use to display the pokemon info
9 | import {PokemonForm} from '../pokemon'
10 |
11 | function PokemonInfo({pokemonName}) {
12 | // 🐨 Have state for the pokemon (null)
13 | // 🐨 use React.useEffect where the callback should be called whenever the
14 | // pokemon name changes.
15 | // 💰 DON'T FORGET THE DEPENDENCIES ARRAY!
16 | // 💰 if the pokemonName is falsy (an empty string) then don't bother making the request (exit early).
17 | // 🐨 before calling `fetchPokemon`, clear the current pokemon state by setting it to null.
18 | // (This is to enable the loading state when switching between different pokemon.)
19 | // 💰 Use the `fetchPokemon` function to fetch a pokemon by its name:
20 | // fetchPokemon('Pikachu').then(
21 | // pokemonData => {/* update all the state here */},
22 | // )
23 | // 🐨 return the following things based on the `pokemon` state and `pokemonName` prop:
24 | // 1. no pokemonName: 'Submit a pokemon'
25 | // 2. pokemonName but no pokemon:
26 | // 3. pokemon:
27 |
28 | // 💣 remove this
29 | return 'TODO'
30 | }
31 |
32 | function App() {
33 | const [pokemonName, setPokemonName] = React.useState('')
34 |
35 | function handleSubmit(newPokemonName) {
36 | setPokemonName(newPokemonName)
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | export default App
51 |
--------------------------------------------------------------------------------
/src/exercise/06.md:
--------------------------------------------------------------------------------
1 | # useEffect: HTTP requests
2 |
3 | ## 📝 Your Notes
4 |
5 | Elaborate on your learnings here in `src/exercise/06.md`
6 |
7 | ## Background
8 |
9 | HTTP requests are another common side-effect that we need to do in applications.
10 | This is no different from the side-effects we need to apply to a rendered DOM or
11 | when interacting with browser APIs like localStorage. In all these cases, we do
12 | that within a `useEffect` hook callback. This hook allows us to ensure that
13 | whenever certain changes take place, we apply the side-effects based on those
14 | changes.
15 |
16 | One important thing to note about the `useEffect` hook is that you cannot return
17 | anything other than the cleanup function. This has interesting implications with
18 | regard to async/await syntax:
19 |
20 | ```javascript
21 | // this does not work, don't do this:
22 | React.useEffect(async () => {
23 | const result = await doSomeAsyncThing()
24 | // do something with the result
25 | })
26 | ```
27 |
28 | The reason this doesn't work is because when you make a function async, it
29 | automatically returns a promise (whether you're not returning anything at all,
30 | or explicitly returning a function). This is due to the semantics of async/await
31 | syntax. So if you want to use async/await, the best way to do that is like so:
32 |
33 | ```javascript
34 | React.useEffect(() => {
35 | async function effect() {
36 | const result = await doSomeAsyncThing()
37 | // do something with the result
38 | }
39 | effect()
40 | })
41 | ```
42 |
43 | This ensures that you don't return anything but a cleanup function.
44 |
45 | 🦉 I find that it's typically just easier to extract all the async code into a
46 | utility function which I call and then use the promise-based `.then` method
47 | instead of using async/await syntax:
48 |
49 | ```javascript
50 | React.useEffect(() => {
51 | doSomeAsyncThing().then(result => {
52 | // do something with the result
53 | })
54 | })
55 | ```
56 |
57 | But how you prefer to do this is totally up to you :)
58 |
59 | ## Exercise
60 |
61 | Production deploys:
62 |
63 | - [Exercise](https://react-hooks.netlify.app/isolated/exercise/06.js)
64 | - [Final](https://react-hooks.netlify.app/isolated/final/06.js)
65 |
66 | In this exercise, we'll be doing data fetching directly in a useEffect hook
67 | callback within our component.
68 |
69 | Here we have a form where users can enter the name of a pokemon and fetch data
70 | about that pokemon. Your job will be to create a component which makes that
71 | fetch request. When the user submits a pokemon name, our `PokemonInfo` component
72 | will get re-rendered with the `pokemonName`
73 |
74 | ## Extra Credit
75 |
76 | ### 1. 💯 handle errors
77 |
78 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-1.js)
79 |
80 | Unfortunately, sometimes things go wrong and we need to handle errors when they
81 | do so we can show the user useful information. Handle that error and render it
82 | out like so:
83 |
84 | ```jsx
85 |
86 | There was an error:
{error.message}
87 |
88 | ```
89 |
90 | You can make an error happen by typing an incorrect pokemon name into the input.
91 |
92 | One common question I get about this extra credit is how to handle promise
93 | errors. There are two ways to do it in this extra credit:
94 |
95 | ```javascript
96 | // option 1: using .catch
97 | fetchPokemon(pokemonName)
98 | .then(pokemon => setPokemon(pokemon))
99 | .catch(error => setError(error))
100 |
101 | // option 2: using the second argument to .then
102 | fetchPokemon(pokemonName).then(
103 | pokemon => setPokemon(pokemon),
104 | error => setError(error),
105 | )
106 | ```
107 |
108 | These are functionally equivalent for our purposes, but they are semantically
109 | different in general.
110 |
111 | Using `.catch` means that you'll handle an error in the `fetchPokemon` promise,
112 | but you'll _also_ handle an error in the `setPokemon(pokemon)` call as well.
113 | This is due to the semantics of how promises work.
114 |
115 | Using the second argument to `.then` means that you will catch an error that
116 | happens in `fetchPokemon` only. In this case, I knew that calling `setPokemon`
117 | would not throw an error (React handles errors and we have an API to catch those
118 | which we'll use later), so I decided to go with the second argument option.
119 |
120 | However, in this situation, it doesn't really make much of a difference. If you
121 | want to go with the safe option, then opt for `.catch`.
122 |
123 | ### 2. 💯 use a status
124 |
125 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-2.js)
126 |
127 | Our logic for what to show the user when is kind of convoluted and requires that
128 | we be really careful about which state we set and when.
129 |
130 | We could make things much simpler by having some state to set the explicit
131 | status of our component. Our component can be in the following "states":
132 |
133 | - `idle`: no request made yet
134 | - `pending`: request started
135 | - `resolved`: request successful
136 | - `rejected`: request failed
137 |
138 | Try to use a status state by setting it to these string values rather than
139 | relying on existing state or booleans.
140 |
141 | Learn more about this concept here:
142 | [Stop using isLoading booleans](https://kentcdodds.com/blog/stop-using-isloading-booleans)
143 |
144 | 💰 Warning: Make sure you call `setPokemon` before calling `setStatus`. We'll
145 | address that more in the next extra credit.
146 |
147 | ### 3. 💯 store the state in an object
148 |
149 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-3.js)
150 |
151 | You'll notice that we're calling a bunch of state updaters in a row. This is
152 | normally not a problem, but each call to our state updater can result in a
153 | re-render of our component. React normally batches these calls so you only get a
154 | single re-render, but it's unable to do this in an asynchronous callback (like
155 | our promise success and error handlers).
156 |
157 | So you might notice that if you do this:
158 |
159 | ```javascript
160 | setStatus('resolved')
161 | setPokemon(pokemon)
162 | ```
163 |
164 | You'll get an error indicating that you cannot read `image` of `null`. This is
165 | because the `setStatus` call results in a re-render that happens before the
166 | `setPokemon` happens.
167 |
168 | > but it's unable to do this in an asynchronous callback
169 |
170 | This is no longer the case in React 18 as it supports automatic batching for
171 | asynchronous callback too.
172 |
173 | Learn more about this concept here:
174 | [New Feature: Automatic Batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching)
175 |
176 | Still it is better to maintain closely related states as an object rather than
177 | maintaining them using individual useState hooks.
178 |
179 | Learn more about this concept here:
180 | [Should I useState or useReducer?](https://kentcdodds.com/blog/should-i-usestate-or-usereducer#conclusion)
181 |
182 | In the future, you'll learn about how `useReducer` can solve this problem really
183 | elegantly, but we can still accomplish this by storing our state as an object
184 | that has all the properties of state we're managing.
185 |
186 | See if you can figure out how to store all of your state in a single object with
187 | a single `React.useState` call so I can update my state like this:
188 |
189 | ```javascript
190 | setState({status: 'resolved', pokemon})
191 | ```
192 |
193 | ### 4. 💯 create an ErrorBoundary component
194 |
195 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-4.js)
196 |
197 | We've already solved the problem for errors in our request, we're only handling
198 | that one error. But there are a lot of different kinds of errors that can happen
199 | in our applications.
200 |
201 | No matter how hard you try, eventually your app code just isn’t going to behave
202 | the way you expect it to and you’ll need to handle those exceptions. If an error
203 | is thrown and unhandled, your application will be removed from the page, leaving
204 | the user with a blank screen... Kind of awkward...
205 |
206 | Luckily for us, there’s a simple way to handle errors in your application using
207 | a special kind of component called an
208 | [Error Boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
209 | Unfortunately, there is currently no way to create an Error Boundary component
210 | with a function and you have to use a class component instead.
211 |
212 | In this extra credit, read up on ErrorBoundary components, and try to create one
213 | that handles this and any other error for the `PokemonInfo` component.
214 |
215 | 💰 to make your error boundary component handle errors from the `PokemonInfo`
216 | component, instead of rendering the error within the `PokemonInfo` component,
217 | you'll need to `throw error` right in the function so React can hand that to the
218 | error boundary. So `if (status === 'rejected') throw error`.
219 |
220 | ### 5. 💯 re-mount the error boundary
221 |
222 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-5.js)
223 |
224 | You might notice that with the changes we've added, we now cannot recover from
225 | an error. For example:
226 |
227 | 1. Type an incorrect pokemon
228 | 2. Notice the error
229 | 3. Type a correct pokemon
230 | 4. Notice it doesn't show that new pokemon's information
231 |
232 | The reason this is happening is because the `error` that's stored in the
233 | internal state of the `ErrorBoundary` component isn't getting reset, so it's not
234 | rendering the `children` we're passing to it.
235 |
236 | So what we need to do is reset the ErrorBoundary's `error` state to `null` so it
237 | will re-render. But how do we access the internal state of our `ErrorBoundary`
238 | to reset it? Well, there are a few ways we could do this by modifying the
239 | `ErrorBoundary`, but one thing you can do when you want to _reset_ the state of
240 | a component, is by providing it a `key` prop which can be used to unmount and
241 | re-mount a component.
242 |
243 | The `key` you can use? Try the `pokemonName`!
244 |
245 | ### 6. 💯 use react-error-boundary
246 |
247 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-6.js)
248 |
249 | As cool as our own `ErrorBoundary` is, I'd rather not have to maintain it in the
250 | long-term. Luckily for us, there's an npm package we can use instead and it's
251 | already installed into this project. It's called
252 | [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary).
253 |
254 | Go ahead and give that a look and swap out our own `ErrorBoundary` for the one
255 | from `react-error-boundary`.
256 |
257 | 💰 It is important to note that `react-error-boundary` do _not_ handle all type
258 | of errors. In our example it works nicely because as mentioned in extra credit 4
259 | that we are throwing error right in the function. It can also happen that some
260 | errors are not handled by `react-error-boundary`. To learn more about how to
261 | handle all type of error you can read
262 | [Handle all errors](https://kentcdodds.com/blog/use-react-error-boundary-to-handle-errors-in-react#handle-all-errors).
263 |
264 | ### 7. 💯 reset the error boundary
265 |
266 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-7.js)
267 |
268 | You may have noticed a problem with the way we're resetting the internal state
269 | of the `ErrorBoundary` using the `key`. Unfortunately, we're not only
270 | re-mounting the `ErrorBoundary`, we're also re-mounting the `PokemonInfo` which
271 | results in a flash of the initial "Submit a pokemon" state whenever we change
272 | our pokemon.
273 |
274 | So let's backtrack on that and instead we'll use `react-error-boundary`'s
275 | `resetErrorBoundary` function (which will be passed to our `ErrorFallback`
276 | component) to reset the state of the `ErrorBoundary` when the user clicks a "try
277 | again" button.
278 |
279 | > 💰 feel free to open up the finished version by clicking the link in the app
280 | > so you can get an idea of how this is supposed to work.
281 |
282 | Once you have this button wired up, we need to react to this reset of the
283 | `ErrorBoundary`'s state by resetting our own state so we don't wind up
284 | triggering the error again. To do this we can use the `onReset` prop of the
285 | `ErrorBoundary`. In that function we can simply `setPokemonName` to an empty
286 | string.
287 |
288 | ### 8. 💯 use resetKeys
289 |
290 | [Production deploy](https://react-hooks.netlify.app/isolated/final/06.extra-8.js)
291 |
292 | Unfortunately now the user can't simply select a new pokemon and continue with
293 | their day. They have to first click "Try again" and then select their new
294 | pokemon. I think it would be cooler if they can just submit a new `pokemonName`
295 | and the `ErrorBoundary` would reset itself automatically.
296 |
297 | Luckily for us `react-error-boundary` supports this with the `resetKeys` prop.
298 | You pass an array of values to `resetKeys` and if the `ErrorBoundary` is in an
299 | error state and any of those values change, it will reset the error boundary.
300 |
301 | 💰 Your `resetKeys` prop should be: `[pokemonName]`
302 |
303 | ## 🦉 Feedback
304 |
305 | Fill out
306 | [the feedback form](https://ws.kcd.im/?ws=React%20Hooks%20%F0%9F%8E%A3&e=06%3A%20useEffect%3A%20HTTP%20requests&em=).
307 |
--------------------------------------------------------------------------------
/src/final/01.extra-1.js:
--------------------------------------------------------------------------------
1 | // useState: greeting
2 | // 💯 accept an initialName
3 | // http://localhost:3000/isolated/final/01.extra-1.js
4 |
5 | import * as React from 'react'
6 |
7 | function Greeting({initialName = ''}) {
8 | const [name, setName] = React.useState(initialName)
9 | function handleChange(event) {
10 | setName(event.target.value)
11 | }
12 | return (
13 |
14 |
18 | {name ? Hello {name} : 'Please type your name'}
19 |
106 | )
107 | }
108 |
109 | function PokemonForm({
110 | pokemonName: externalPokemonName,
111 | initialPokemonName = externalPokemonName || '',
112 | onSubmit,
113 | }) {
114 | const [pokemonName, setPokemonName] = React.useState(initialPokemonName)
115 |
116 | // this is generally not a great idea. We're synchronizing state when it is
117 | // normally better to derive it https://kentcdodds.com/blog/dont-sync-state-derive-it
118 | // however, we're doing things this way to make it easier for the exercises
119 | // to not have to worry about the logic for this PokemonForm component.
120 | React.useEffect(() => {
121 | // note that because it's a string value, if the externalPokemonName
122 | // is the same as the one we're managing, this will not trigger a re-render
123 | if (typeof externalPokemonName === 'string') {
124 | setPokemonName(externalPokemonName)
125 | }
126 | }, [externalPokemonName])
127 |
128 | function handleChange(e) {
129 | setPokemonName(e.target.value)
130 | }
131 |
132 | function handleSubmit(e) {
133 | e.preventDefault()
134 | onSubmit(pokemonName)
135 | }
136 |
137 | function handleSelect(newPokemonName) {
138 | setPokemonName(newPokemonName)
139 | onSubmit(newPokemonName)
140 | }
141 |
142 | return (
143 |
185 | )
186 | }
187 |
188 | function ErrorFallback({error, resetErrorBoundary}) {
189 | return (
190 |