diff --git a/src/__tests__/01.tsx b/src/__tests__/01.tsx deleted file mode 100644 index 6f5acf87..00000000 --- a/src/__tests__/01.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' -import {render, screen} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/01' -// import App from '../exercise/01' -import react from 'react' - -// don't do this in regular tests! -const Counter = App().type - -jest.mock('react', () => {return { - ...jest.requireActual('react'), - useReducer: jest.fn(), - useState: jest.fn(), - } -}) - -if (!Counter) { - alfredTip( - true, - `Can't find the Counter from the exported App component. Please make sure to not edit the App component so I can find the Counter and run some tests on it.`, - ) -} - -beforeEach(() => { - const {useReducer, useState} = jest.requireActual('react') - react.useReducer.mockImplementation(useReducer) - react.useState.mockImplementation(useState) -}) - -test('clicking the button increments the count with useReducer', async () => { - const {container} = render() - const increment = screen.getByRole('button', {name: '➡️'}) - const decrement = screen.getByRole('button', {name: '⬅️'}) - - userEvent.click(increment) - expect(container).toHaveTextContent('1') - userEvent.click(decrement) - expect(container).toHaveTextContent('0') - - alfredTip(() => { - const commentLessLines = (Counter as Function) - .toString() - .split('\n') - .filter(l => !l.trim().substr(0, 2).includes('//')) - .join('\n') - expect(commentLessLines).toMatch('useReducer(') - expect(commentLessLines).not.toMatch('useState(') - }, 'The Counter component that is rendered must call "useReducer" and not "useState" to get the "state" and "dispatch" function and you should get rid of that useState call.') -}) diff --git a/src/__tests__/02.extra-3.js b/src/__tests__/02.extra-3.js deleted file mode 100644 index b5e90e6a..00000000 --- a/src/__tests__/02.extra-3.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react' -import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' -import {render, screen, act} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/02.extra-3' -// import App from '../exercise/02' - -beforeEach(() => { - jest.spyOn(window, 'fetch') - jest.spyOn(console, 'error') -}) - -afterEach(() => { - window.fetch.mockRestore() - console.error.mockRestore() -}) - -test('displays the pokemon', async () => { - const {unmount} = render() - const input = screen.getByLabelText(/pokemon/i) - const submit = screen.getByText(/^submit$/i) - - // verify that an initial request is made when mounted - await userEvent.type(input, 'pikachu') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /pikachu/i}) - - // verify that a request is made when props change - await userEvent.clear(input) - await userEvent.type(input, 'ditto') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - // verify that when props remain the same a request is not made - window.fetch.mockClear() - - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - alfredTip( - () => expect(window.fetch).not.toHaveBeenCalled(), - 'Make certain that you are providing a dependencies list in useEffect!', - ) - - // verify error handling - console.error.mockImplementation(() => {}) - - await userEvent.clear(input) - await userEvent.type(input, 'george') - await userEvent.click(submit) - expect(await screen.findByRole('alert')).toHaveTextContent( - /There was an error.*Unsupported pokemon.*george/, - ) - expect(console.error).toHaveBeenCalledTimes(3) - - // restore the original implementation - console.error.mockRestore() - // but we still want to make sure it's not called - jest.spyOn(console, 'error') - - await userEvent.type(input, 'mew') - await userEvent.click(submit) - - // verify unmounting does not result in an error - unmount() - // wait for a bit for the mocked request to resolve: - await act(() => new Promise(r => setTimeout(r, 100))) - alfredTip( - () => expect(console.error).not.toHaveBeenCalled(), - 'Make sure that when the component is unmounted the component does not attempt to trigger a rerender with `dispatch`', - ) -}) diff --git a/src/__tests__/02.tsx b/src/__tests__/02.tsx deleted file mode 100644 index 8af3d07f..00000000 --- a/src/__tests__/02.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react' -import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' -import {render, screen} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/02' -// import App from '../exercise/02' - -const getFetchMock = () => jest.spyOn(window, 'fetch') -const getErrorMock = () => jest.spyOn(console, 'error') -let fetchMock: ReturnType, - errorMock: ReturnType -beforeEach(() => { - fetchMock = getFetchMock() - errorMock = getErrorMock() -}) - -afterEach(() => { - fetchMock.mockRestore() - errorMock.mockRestore() -}) - -test('displays the pokemon', async () => { - render() - const input = screen.getByLabelText(/pokemon/i) - const submit = screen.getByText(/^submit$/i) - - // verify that an initial request is made when mounted - await userEvent.type(input, 'pikachu') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /pikachu/i}) - - // verify that a request is made when props change - await userEvent.clear(input) - await userEvent.type(input, 'ditto') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - // verify that when props remain the same a request is not made - fetchMock.mockClear() - - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - alfredTip( - () => expect(fetchMock).not.toHaveBeenCalled(), - 'Make certain that you are providing a dependencies list in useEffect!', - ) - - // verify error handling - errorMock.mockImplementation(() => {}) - - await userEvent.clear(input) - await userEvent.type(input, 'george') - await userEvent.click(submit) - expect(await screen.findByRole('alert')).toHaveTextContent( - /There was an error.*Unsupported pokemon.*george/, - ) - expect(errorMock).toHaveBeenCalledTimes(2) - - errorMock.mockReset() -}) diff --git a/src/__tests__/03.extra-2.tsx b/src/__tests__/03.extra-2.tsx deleted file mode 100644 index 63d6e4e5..00000000 --- a/src/__tests__/03.extra-2.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react' -import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' -import {render, screen} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/03.extra-2' -// import App from '../exercise/03.extra-2' - -const getFetchMock = () => jest.spyOn(window, 'fetch') -const getErrorMock = () => jest.spyOn(console, 'error') -let fetchMock: ReturnType, - errorMock: ReturnType -beforeEach(() => { - fetchMock = getFetchMock() - errorMock = getErrorMock() -}) - -afterEach(() => { - fetchMock.mockRestore() - errorMock.mockRestore() -}) - -test('displays the pokemon', async () => { - render() - const input = screen.getByLabelText(/pokemon/i) - const submit = screen.getByText(/^submit$/i) - - // verify that an initial request is made when mounted - await userEvent.type(input, 'pikachu') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /pikachu/i}) - - // verify that a request is made when props change - await userEvent.clear(input) - await userEvent.type(input, 'ditto') - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - // verify that when props remain the same a request is not made - fetchMock.mockClear() - - await userEvent.click(submit) - - await screen.findByRole('heading', {name: /ditto/i}) - - alfredTip( - () => expect(fetchMock).not.toHaveBeenCalled(), - 'Make certain that you are providing a dependencies list in useEffect!', - ) - - // verify error handling - errorMock.mockImplementation(() => {}) - - await userEvent.clear(input) - await userEvent.type(input, 'george') - await userEvent.click(submit) - expect(await screen.findByRole('alert')).toHaveTextContent( - /There was an error.*Unsupported pokemon.*george/, - ) - expect(errorMock).toHaveBeenCalledTimes(2) - - errorMock.mockReset() - fetchMock.mockClear() - - // use the cached value - userEvent.click(screen.getByRole('button', {name: /ditto/i})) - expect(fetchMock).not.toHaveBeenCalled() - await screen.findByRole('heading', {name: /ditto/i}) -}) diff --git a/src/__tests__/03.tsx b/src/__tests__/03.tsx deleted file mode 100644 index a23d2e92..00000000 --- a/src/__tests__/03.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react' -import {render, screen} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/03' -// import App from '../exercise/03' - -test('clicking the button increments the count', async () => { - render() - const button = screen.getByText(/increment count/i) - const display = screen.getByText(/the current count/i) - expect(display).toHaveTextContent(/0/) - await userEvent.click(button) - expect(display).toHaveTextContent(/1/) - await userEvent.click(button) - expect(display).toHaveTextContent(/2/) -}) diff --git a/src/__tests__/04.tsx b/src/__tests__/04.tsx deleted file mode 100644 index d20f0db2..00000000 --- a/src/__tests__/04.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react' -import {render} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/04' -// import App from '../exercise/04' - -test('adds and removes children from the log', async () => { - const {getByText, getByRole} = render() - const log = getByRole('log') - const chatCount = log.children.length - const add = getByText(/add/i) - const remove = getByText(/remove/i) - await userEvent.click(add) - expect(log.children).toHaveLength(chatCount + 1) - await userEvent.click(remove) - expect(log.children).toHaveLength(chatCount) -}) - -test('scrolls to the bottom', async () => { - const {getByText, getByRole} = render() - const log = getByRole('log') - const add = getByText(/add/i) - const remove = getByText(/remove/i) - const scrollTopSetter = jest.fn() - Object.defineProperties(log, { - scrollHeight: { - get() { - return 100 - }, - }, - scrollTop: { - get() { - return 0 - }, - set: scrollTopSetter, - }, - }) - - await userEvent.click(add) - expect(scrollTopSetter).toHaveBeenCalledTimes(1) - expect(scrollTopSetter).toHaveBeenCalledWith(log.scrollHeight) - - scrollTopSetter.mockClear() - - await userEvent.click(remove) - expect(scrollTopSetter).toHaveBeenCalledTimes(1) - expect(scrollTopSetter).toHaveBeenCalledWith(log.scrollHeight) -}) diff --git a/src/__tests__/05.tsx b/src/__tests__/05.tsx deleted file mode 100644 index 94d12eb3..00000000 --- a/src/__tests__/05.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react' -import {render} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import App from '../final/05' -// import App from '../exercise/05' - -test('adds and removes children from the log', async () => { - const {getByText, getByRole} = render() - const log = getByRole('log') - const chatCount = log.children.length - const add = getByText(/add/i) - const remove = getByText(/remove/i) - await userEvent.click(add) - expect(log.children).toHaveLength(chatCount + 1) - await userEvent.click(remove) - expect(log.children).toHaveLength(chatCount) -}) - -test('scroll to top scrolls to the top', async () => { - const {getByText, getByRole} = render() - const log = getByRole('log') - const scrollToTop = getByText(/scroll to top/i) - const scrollToBottom = getByText(/scroll to bottom/i) - const scrollTopSetter = jest.fn() - Object.defineProperties(log, { - scrollHeight: { - get() { - return 100 - }, - }, - scrollTop: { - get() { - return 0 - }, - set: scrollTopSetter, - }, - }) - await userEvent.click(scrollToTop) - expect(scrollTopSetter).toHaveBeenCalledTimes(1) - expect(scrollTopSetter).toHaveBeenCalledWith(0) - - scrollTopSetter.mockClear() - - await userEvent.click(scrollToBottom) - expect(scrollTopSetter).toHaveBeenCalledTimes(1) - expect(scrollTopSetter).toHaveBeenCalledWith(log.scrollHeight) -}) diff --git a/src/__tests__/06.extra-1.js b/src/__tests__/06.extra-1.js deleted file mode 100644 index 0d990173..00000000 --- a/src/__tests__/06.extra-1.js +++ /dev/null @@ -1,41 +0,0 @@ -import matchMediaPolyfill from 'mq-polyfill' -import * as React from 'react' -import {render, act} from '@testing-library/react' -import App from '../final/06.extra-1' -// import App from '../exercise/06' - -beforeAll(() => { - matchMediaPolyfill(window) - window.resizeTo = function resizeTo(width, height) { - Object.assign(this, { - innerWidth: width, - innerHeight: height, - outerWidth: width, - outerHeight: height, - }).dispatchEvent(new this.Event('resize')) - } -}) - -// sorry, I just couldn't find a reliable way to test your implementation -// so this test just ensures you don't break anything 😅 - -test('works', async () => { - const {container} = render() - - const box = container.querySelector('[style]') - - act(() => { - window.resizeTo(1001, 1001) - }) - expect(box).toHaveStyle(`background-color: green;`) - - act(() => { - window.resizeTo(800, 800) - }) - expect(box).toHaveStyle(`background-color: yellow;`) - - act(() => { - window.resizeTo(600, 600) - }) - expect(box).toHaveStyle(`background-color: red;`) -}) diff --git a/src/__tests__/06.tsx b/src/__tests__/06.tsx deleted file mode 100644 index c17c5e08..00000000 --- a/src/__tests__/06.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import matchMediaPolyfill from 'mq-polyfill' -import * as React from 'react' -import {render, act} from '@testing-library/react' -import App from '../final/06' -// import App from '../exercise/06' - -beforeAll(() => { - matchMediaPolyfill(window) - window.resizeTo = function resizeTo(width, height) { - Object.assign(this, { - innerWidth: width, - innerHeight: height, - outerWidth: width, - outerHeight: height, - }).dispatchEvent(new this.Event('resize')) - } -}) - -// sorry, I just couldn't find a reliable way to test your implementation -// so this test just ensures you don't break anything 😅 - -test('works', async () => { - const {container} = render() - - const box = container.querySelector('[style]') - - act(() => { - window.resizeTo(1001, 1001) - }) - expect(box).toHaveStyle(`background-color: green;`) - - act(() => { - window.resizeTo(800, 800) - }) - expect(box).toHaveStyle(`background-color: yellow;`) - - act(() => { - window.resizeTo(600, 600) - }) - expect(box).toHaveStyle(`background-color: red;`) -}) diff --git a/src/exercise/02.md b/src/exercise/02.md deleted file mode 100644 index fc3195e1..00000000 --- a/src/exercise/02.md +++ /dev/null @@ -1,461 +0,0 @@ -# useCallback: custom hooks - -## 📝 Your Notes - -Elaborate on your learnings here in `src/exercise/02.md` - -## Background - -### Memoization in general - -Memoization: a performance optimization technique which eliminates the need to -recompute a value for a given input by storing the original computation and -returning that stored value when the same input is provided. Memoization is a -form of caching. Here's a simple implementation of memoization: - -```typescript -const values = {} -function addOne(num: number) { - if (values[num] === undefined) { - values[num] = num + 1 // <-- here's the computation - } - return values[num] -} -``` - -One other aspect of memoization is value referential equality. For example: - -```typescript -const dog1 = new Dog('sam') -const dog2 = new Dog('sam') -console.log(dog1 === dog2) // false -``` - -Even though those two dogs have the same name, they are not the same. However, -we can use memoization to get the same dog: - -```typescript -const dogs = {} -function getDog(name: string) { - if (dogs[name] === undefined) { - dogs[name] = new Dog(name) - } - return dogs[name] -} - -const dog1 = getDog('sam') -const dog2 = getDog('sam') -console.log(dog1 === dog2) // true -``` - -You might have noticed that our memoization examples look very similar. -Memoization is something you can implement as a generic abstraction: - -```typescript -function memoize(cb: (arg: ArgType) => ReturnValue) { - const cache: Record = {} - return function memoized(arg: ArgType) { - if (cache[arg] === undefined) { - cache[arg] = cb(arg) - } - return cache[arg] - } -} - -const addOne = memoize((num: number) => num + 1) -const getDog = memoize((name: string) => new Dog(name)) -``` - -Our abstraction only supports one argument, if you want to make it work for any -type/number of arguments, knock yourself out. - -### Memoization in React - -Luckily, in React we don't have to implement a memoization abstraction. They -made two for us! `useMemo` and `useCallback`. For more on this read: -[Memoization and React](https://epicreact.dev/memoization-and-react). - -You know the dependency list of `useEffect`? Here's a quick refresher: - -```tsx -React.useEffect(() => { - window.localStorage.setItem('count', count) -}, [count]) // <-- that's the dependency list -``` - -Remember that the dependency list is how React knows whether to call your -callback (and if you don't provide one then React will call your callback every -render). It does this to ensure that the side effect you're performing in the -callback doesn't get out of sync with the state of the application. - -But what happens if I use a function in my callback? - -```tsx -const updateLocalStorage = () => window.localStorage.setItem('count', count) -React.useEffect(() => { - updateLocalStorage() -}, []) // <-- what goes in that dependency list? -``` - -We could just put the `count` in the dependency list and that would -actually/accidentally work, but what would happen if one day someone were to -change `updateLocalStorage`? - -```diff -- const updateLocalStorage = () => window.localStorage.setItem('count', count) -+ const updateLocalStorage = () => window.localStorage.setItem(key, count) -``` - -Would we remember to update the dependency list to include the `key`? Hopefully -we would. But this can be a pain to keep track of dependencies. Especially if -the function that we're using in our `useEffect` callback is coming to us from -props (in the case of a custom component) or arguments (in the case of a custom -hook). - -Instead, it would be much easier if we could just put the function itself in the -dependency list: - -```javascript -const updateLocalStorage = () => window.localStorage.setItem('count', count) -React.useEffect(() => { - updateLocalStorage() -}, [updateLocalStorage]) // <-- function as a dependency -``` - -The problem with doing that is that it will trigger the `useEffect` to run every -render. This is because `updateLocalStorage` is defined inside the component -function body. So it's re-initialized every render. Which means it's brand new -every render. Which means it changes every render. Which means... you guessed -it, our `useEffect` callback will be called every render! - -**This is the problem `useCallback` solves**. And here's how you solve it - -```tsx -const updateLocalStorage = React.useCallback( - () => window.localStorage.setItem('count', count), - [count], // <-- yup! That's a dependency list! -) -React.useEffect(() => { - updateLocalStorage() -}, [updateLocalStorage]) -``` - -What that does is we pass React a function and React gives that same function -back to us... Sounds kinda useless right? Imagine: - -```tsx -// this is not how React actually implements this function. We're just imaginging! -function useCallback(callback: Function) { - return callback -} -``` - -Uhhh... But there's a catch! On subsequent renders, if the elements in the -dependency list are unchanged, instead of giving the same function back that we -give to it, React will give us the same function it gave us last time. So -imagine: - -```tsx -// this is not how React actually implements this function. We're just imaginging! -let lastCallback -function useCallback(callback: Function, deps: Array) { - if (depsChanged(deps)) { - lastCallback = callback - return callback - } else { - return lastCallback - } -} -``` - -So while we still create a new function every render (to pass to `useCallback`), -React only gives us the new one if the dependency list changes. - -In this exercise, we're going to be using `useCallback`, but `useCallback` is -just a shortcut to using `useMemo` for functions: - -```typescript -// the useMemo version: -const updateLocalStorage = React.useMemo( - // useCallback saves us from this annoying double-arrow function thing: - () => () => window.localStorage.setItem('count', count), - [count], -) - -// the useCallback version -const updateLocalStorage = React.useCallback( - () => window.localStorage.setItem('count', count), - [count], -) -``` - -🦉 A common question with this is: "Why don't we just wrap every function in -`useCallback`?" You can read about this in my blog post -[When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback). - -🦉 And if the concept of a "closure" is new or confusing to you, then -[give this a read](https://mdn.io/closure). (Closures are one of the reasons -it's important to keep dependency lists correct.) - -## Exercise - -Production deploys: - -- [Exercise](https://advanced-react-hooks.netlify.com/isolated/exercise/02.tsx) -- [Final](https://advanced-react-hooks.netlify.com/isolated/final/02.tsx) - -**People tend to find this exercise more difficult,** so I strongly advise -spending some time understanding how the code works before making any changes! - -Also, one thing to keep in mind is that React hooks are a great foundation upon -which to build libraries and many have been built. For that reason, you don't -often need to go this deep into making custom hooks. So if you find this one -isn't clicking for you, know that you _are_ learning and when you _do_ face a -situation when you need to use this knowledge, you'll be able to come back and -it will click right into place. - -👨‍💼 Peter the Product Manager told us that we've got more features coming our way -that will require managing async state. We've already got some code for our -pokemon lookup feature (if you've gone through the "React Hooks" workshop -already, then this should be familiar, if not, spend some time playing with the -app to get up to speed with what we're dealing with here). We're going to -refactor out the async logic so we can reuse this in other areas of the app. - -**So, your job is** to extract the logic from the `PokemonInfo` component into a -custom and generic `useAsync` hook. In the process you'll find you need to do -some fancy things with dependencies (dependency arrays are the biggest challenge -to deal with when making custom hooks). - -NOTE: In this part of the exercise, we don't need `useCallback`. We'll add it in -the extra credits. It's important that you work on this refactor first so you -can appreciate the value `useCallback` provides in certain circumstances. - -## Extra Credit - -### 1. 💯 use useCallback to empower the user to customize memoization - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/02.extra-1.tsx) - -Unfortunately, the ESLint plugin is unable to determine whether the -`dependencies` argument is a valid argument for `useEffect` which is a shame, -and normally I'd say just ignore it and move on. But, there's another solution -to this problem which I think is probably better. - -Instead of accepting `dependencies` to `useAsync`, why don't we just treat the -`asyncCallback` as a dependency? Any time `asyncCallback` changes, we know that -we should call it again. The problem is that because our `asyncCallback` depends -on the `pokemonName` which comes from props, it has to be defined within the -body of the component, which means that it will be defined on every render which -means it will be new every render. This is where `React.useCallback` comes in! - -Here's another example of the `React.useCallback` API: - -```tsx -function ConsoleGreeting(props) { - const greet = React.useCallback( - greeting => console.log(`${greeting} ${props.name}`), - [props.name], - ) - - React.useEffect(() => { - const helloGreeting = 'Hello' - greet(helloGreeting) - }, [greet]) - return
check the console
-} -``` - -The first argument to `useCallback` is the callback you want called, the second -argument is an array of dependencies which is similar to `useEffect`. When one -of the dependencies changes between renders, the callback you passed in the -first argument will be the one returned from `useCallback`. If they do not -change, then you'll get the callback which was returned the previous time (so -the callback remains the same between renders). - -So we only want our `asyncCallback` to change when the `pokemonName` changes. -See if you can make things work like this: - -```tsx -// 🐨 you'll need to wrap asyncCallback in React.useCallback -function asyncCallback() { - if (!pokemonName) { - return - } - return fetchPokemon(pokemonName) -} - -// 🐨 you'll need to update useAsync to remove the dependencies and list the -// async callback as a dependency. -const state = useAsync(asyncCallback) -``` - -### 2. 💯 return a memoized `run` function from useAsync - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/02.extra-2.tsx) - -Requiring users to provide a memoized value is fine. You can document it as part -of the API and expect people to just read the docs right? lol, that's hilarious -😂 It'd be WAY better if we could redesign the API a bit so we (as the hook -developers) are the ones who have to memoize the function, and the users of our -hook don't have to worry about it. - -So see if you can redesign this a little bit by providing a (memoized) `run` -function that people can call in their own `useEffect` like this: - -```tsx -// 💰 destructuring this here now because it just felt weird to call this -// "state" still when it's also returning a function called "run" 🙃 -const { - data: pokemon, - status, - error, - run, -} = useAsync({status: pokemonName ? 'pending' : 'idle'}) - -React.useEffect(() => { - if (!pokemonName) { - return - } - // 💰 note the absence of `await` here. We're literally passing the promise - // to `run` so `useAsync` can attach it's own `.then` handler on it to keep - // track of the state of the promise. - const pokemonPromise = fetchPokemon(pokemonName) - run(pokemonPromise) -}, [pokemonName, run]) -``` - -### 3. 💯 avoid race conditions - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/02.extra-3.tsx) - -When dealing with anything asynchronous, you can sometimes run into what are -called "race conditions." A race condition is a situation where two or more -related asynchronous tasks complete in an unexpected order leading to an -undesired result. - -We have a race condition in our code, but you can't tell because our API is -mocked. If you want to experience it, then update this line: - -```diff -- run(fetchPokemon(pokemonName)) -+ run(fetchPokemon(pokemonName, {delay: Math.random() * 5000})) -``` - -What this will do is make it so the mocked backend will take anywhere from 0ms -to 5000ms to complete our request. Now, click two pokemon in rapid succession. -Do it again, and again. Eventually you'll observe the problem. Sometimes the one -you picked first, shows up last! - -The correct thing to do here is always focus on the pokemon that was selected -last. To do this, we'll keep track of the latest promise in our state object and -only update the state if the dispatch is coming from the latest promise. - -💰 This one is a bit tricky, so I'm going to give you the type updates for our -reducer's `State` and `Action` types: - -```tsx -type AsyncState = - | { - status: 'idle' - data?: null - error?: null - promise?: null - } - | { - status: 'pending' - data?: null - error?: null - promise: Promise - } - | { - status: 'resolved' - data: DataType - error: null - promise: null - } - | { - status: 'rejected' - data: null - error: Error - promise: null - } - -type AsyncAction = - | {type: 'reset'} - | {type: 'pending'; promise: Promise} - | {type: 'resolved'; data: DataType; promise: Promise} - | {type: 'rejected'; error: Error; promise: Promise} -``` - -That should be enough to get you going! - -### 4. 💯 abort unused requests - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/02.extra-4.tsx) - -If you open the network tab as you're switching pokemon, you'll notice that even -though we're ignoring the pokemon that's not the latest choice, the browser is -still downloading and parsing that data as `JSON`. This is not a big deal for -our small app, but it could be a problem for other situations you may face. - -Luckily for us, there's a way to tell the browser to cancel fetch requests: -[`AbortController`s](https://mdn.io/abortcontroller)! Here's a simple example of -using an abort controller with `fetch`: - -```tsx -// create the abort controller: -const abortController = new AbortController() - -// send off the request: -const promise = fetch('some-api', {signal: abortController.signal}) - -// oh no! Nevermind! We don't want to waste resources on that request anymore: -abortController.abort() - -// 💥 NOTE: the promise is rejected because it's impossible to "cancel" a promise -// luckily for us, we won't have to worry about this thanks to the way we've -// coded our reducer, but you may need to handle the rejected promise in other -// situations. -``` - -It's important to know that once the request is sent we can't tell the server to -stop processing stuff. That request is gone and there's nothing we can do about -it. All the abort controller does is tell the browser to "ignore" the response. - -Our `fetchPokemon` utility has support for `signal` built-in already: - -```tsx -fetchPokemon(pokemonName, {signal: abortController.signal}) -``` - -So all you need to do is create the abort controller and then abort it in the -event the user changes their mind and the `pokemonName` changes. 💰 This is the -perfect situation for the `useEffect` cleanup function. - -💰 Try not to overthink this one. There are only a couple of line changes -necessary compared to the previous extra credit and they're all in the -`useEffect`. - -## 🦉 Other notes - -### `useEffect` and `useCallback` - -The use case for `useCallback` in the exercise is a perfect example of the types -of problems `useCallback` is intended to solve. However the examples in these -instructions are intentionally contrived. You can simplify things a great deal -by _not_ extracting code from `useEffect` into functions that you then have to -memoize with `useCallback`. Read more about this here: -[Myths about useEffect](https://epicreact.dev/myths-about-useeffect). - -### `useCallback` use cases - -The entire purpose of `useCallback` is to memoize a callback for use in -dependency lists and props on memoized components (via `React.memo`, which you -can learn more about from the performance workshop). The _only_ time it's useful -to use `useCallback` is when the function you're memoizing is used in one of -those two situations. - -## 🦉 Feedback - -Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=02%3A%20useCallback%3A%20custom%20hooks&em=). diff --git a/src/exercise/02.tsx b/src/exercise/02.tsx deleted file mode 100644 index a17b3e11..00000000 --- a/src/exercise/02.tsx +++ /dev/null @@ -1,168 +0,0 @@ -// useCallback: custom hooks -// http://localhost:3000/isolated/exercise/02.tsx - -import * as React from 'react' -import { - fetchPokemon, - PokemonForm, - PokemonDataView, - PokemonInfoFallback, - PokemonErrorBoundary, -} from '../pokemon' -import {PokemonData} from '../types' - -// 🦺 I'd change "PokemonState" to "AsyncState" -// Also rename the "pokemon" property to a more generic name like "data" -// 🦺 Also, now that we're making a "generic" hook, -// we'll want this type to be a generic that takes a DataType and uses that -// instead of "PokemonData" -type PokemonState = - | { - status: 'idle' - pokemon?: null - error?: null - } - | { - status: 'pending' - pokemon?: null - error?: null - } - | { - status: 'resolved' - pokemon: PokemonData - error: null - } - | { - status: 'rejected' - pokemon: null - error: Error - } - -// 🦺 similar to above, this will need to be a generic type now and rename "pokemon" to "data" -// I'd also recommend renaming this -type PokemonAction = - | {type: 'reset'} - | {type: 'pending'} - | {type: 'resolved'; pokemon: PokemonData} - | {type: 'rejected'; error: Error} - -// 🐨 this is going to be our generic asyncReducer -// 🦺 make this function a generic that accepts a DataType and passes that to -// your AsyncState and AsyncAction types -function pokemonInfoReducer( - state: PokemonState, - action: PokemonAction, -): PokemonState { - switch (action.type) { - case 'pending': { - // 🐨 replace "pokemon" with "data" - return {status: 'pending', pokemon: null, error: null} - } - case 'resolved': { - // 🐨 replace "pokemon" with "data" (in the action too!) - return {status: 'resolved', pokemon: action.pokemon, error: null} - } - case 'rejected': { - // 🐨 replace "pokemon" with "data" - return {status: 'rejected', pokemon: null, error: action.error} - } - default: { - throw new Error(`Unhandled action type: ${action.type}`) - } - } -} - -function PokemonInfo({pokemonName}) { - // 🐨 move all the code between the lines into a new useAsync function. - // 💰 look below to see how the useAsync hook is supposed to be called - // 🦺 useAsync will need to be a generic that takes the DataType which can - // be inferred from the asyncCallback. - // 💰 If you want some help, here's the function signature (or delete this - // comment really quick if you don't want the spoiler)! - // function useAsync( - // asyncCallback: () => Promise | null, - // dependencies: Array, - // ) {/* code in here */} - - // -------------------------- start -------------------------- - - const [state, dispatch] = React.useReducer(pokemonInfoReducer, { - status: 'idle', - // 🐨 this will need to be "data" instead of "pokemon" - pokemon: null, - error: null, - }) - - React.useEffect(() => { - // 💰 this first early-exit bit is a little tricky, so let me give you a hint: - // const promise = asyncCallback() - // if (!promise) { - // return - // } - // then you can dispatch and handle the promise etc... - if (!pokemonName) { - return - } - dispatch({type: 'pending'}) - fetchPokemon(pokemonName).then( - pokemon => { - dispatch({type: 'resolved', pokemon}) - }, - error => { - dispatch({type: 'rejected', error}) - }, - ) - // 🐨 you'll accept dependencies as an array and pass that here. - // 🐨 because of limitations with ESLint, you'll need to ignore - // the react-hooks/exhaustive-deps rule. We'll fix this in an extra credit. - }, [pokemonName]) - // --------------------------- end --------------------------- - - // 🐨 here's how you'll use the new useAsync hook you're writing: - // const state = useAsync(() => { - // if (!pokemonName) { - // return - // } - // return fetchPokemon(pokemonName) - // }, {/* initial state */}, [pokemonName]) - // 🐨 this will change from "pokemon" to "data" - const {pokemon, status, error} = state - - switch (status) { - case 'idle': - return Submit a pokemon - case 'pending': - return - case 'rejected': - throw error - case 'resolved': - return - default: - throw new Error('This should be impossible') - } -} - -function App() { - const [pokemonName, setPokemonName] = React.useState('') - - function handleSubmit(newPokemonName: string) { - setPokemonName(newPokemonName) - } - - function handleReset() { - setPokemonName('') - } - - return ( -
- -
-
- - - -
-
- ) -} -export default App diff --git a/src/exercise/03.md b/src/exercise/03.md deleted file mode 100644 index 4be6bdbf..00000000 --- a/src/exercise/03.md +++ /dev/null @@ -1,177 +0,0 @@ -# useContext: simple Counter - -## 📝 Your Notes - -Elaborate on your learnings here in `src/exercise/03.md` - -## Background - -Sharing state between components is a common problem. The best solution for this -is to 📜 [lift your state](https://react.dev/learn/sharing-state-between-components). This -requires 📜 [prop drilling](https://kentcdodds.com/blog/prop-drilling) which is -not a problem, but there are some times where prop drilling can cause a real -pain. - -To avoid this pain, we can insert some state into a section of our react tree, -and then extract that state anywhere within that react tree without having to -explicitly pass it everywhere. This feature is called `context`. In some ways -it's like global variables, but it doesn't suffer from the same problems (and -maintainability nightmares) of global variables thanks to how the API works to -make the relationships explicit. - -Here's how you use context: - -```javascript -import * as React from 'react' - -const FooContext = React.createContext() - -function FooDisplay() { - const foo = React.useContext(FooContext) - return
Foo is: {foo}
-} - -ReactDOM.render( - - - , - document.getElementById('root'), -) -// renders
Foo is: I am foo
-``` - -`` could appear anywhere in the render tree, and it will have -access to the `value` which is passed by the `FooContext.Provider` component. - -Note that as a first argument to `createContext`, you can provide a default -value which React will use in the event someone calls `useContext` with your -context, when no value has been provided: - -```javascript -ReactDOM.render(, document.getElementById('root')) -``` - -Most of the time, I don't recommend using a default value because it's probably -a mistake to try and use context outside a provider, so in our exercise I'll -show you how to avoid that from happening. - -🦉 Keep in mind that while context makes sharing state easy, it's not the only -solution to Prop Drilling pains and it's not necessarily the best solution -either. React's composition model is powerful and can be used to avoid issues -with prop drilling as well. Learn more about this from -[Michael Jackson on Twitter](https://twitter.com/mjackson/status/1195495535483817984) - -## Exercise - -Production deploys: - -- [Exercise](https://advanced-react-hooks.netlify.com/isolated/exercise/03.tsx) -- [Final](https://advanced-react-hooks.netlify.com/isolated/final/03.tsx) - -We're putting everything in one file to keep things simple, but I've labeled -things a bit so you know that typically your context provider will be placed in -a different file and expose the provider component itself as well as the custom -hook to access the context value. - -We're going to take the Count component that we had before and separate the -button from the count display. We need to access both the `count` state as well -as the `setCount` updater in these different components which live in different -parts of the tree. Normally lifting state up would be the way to solve this -trivial problem, but this is a contrived example so you can focus on learning -how to use context. - -Your job is to fill in the `CountProvider` function component so that the app -works and the tests pass. - -## Extra Credit - -### 1. 💯 create a consumer hook - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/03.extra-1.tsx) - -Imagine what would happen if someone tried to consume your context value without -using your context provider. For example, as mentioned above when discussing the -default value: - -```javascript -ReactDOM.render(, document.getElementById('root')) -``` - -If you don't provide a default context value, that would render -`
Foo is:
`. This is because the context value would be `undefined`. -In real-world scenarios, having an unexpected `undefined` value can result in -errors that can be difficult to debug. - -In this extra credit, you need to create a custom hook that I can use like this: - -```javascript -// inside CountDisplay/Counter for example -const [count, setCount] = useCount() -``` - -And if you change the `App` to this: - -```javascript -function App() { - return ( -
- - -
- ) -} -``` - -It should throw an error indicating that `useCount` may only be used from within a (child of a) -CountProvider. - -### 2. 💯 caching in a context provider - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/03.extra-2.tsx) - -Let's try the last exercise over again with a bit more of a complex/practical -example. That's right! We're back to the Pokemon info app! This time it has -caching in place which is cool. So if you enter the same pokemon information, -it's cached so it loads instantaneously. - -However, we have a requirement that we want to list all the cached pokemon in -another part of the app, so we're going to use context to store the cache. This -way both parts of the app which need access to the pokemon cache will have -access. - -Because this is hard to describe in words (and because it's a completely -separate example), there's a starting point for you in -`./src/exercise/03.extra-2.js`. - -### 3. 💯 Remove context - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/03.extra-3.tsx) - -NOTE: This extra credit builds on top of what you just did in -`./src/exercise/03.extra-2.js`, so work on this one in there. - -Sometimes you really just don't need context, especially when passing props a -few levels isn't a big deal. So in this extra credit, you're going to remove -context and pass props instead. You'll delete the `createContext`, -`usePokemonCache`, and `PokemonCacheProvider` stuff and do the -`const [cache, dispatch] = React.useReducer(pokemonCacheReducer, {})` directly -inside the `PokemonSection` component. - -This is just to illustrate that sometimes you don't need context. The biggest -use case for context is for libraries that need to implicitly share state -between components and you can learn more about this in the Advanced React -Patterns workshop. - -## 🦉 Other notes - -`Context` also has the unique ability to be scoped to a specific section of the -React component tree. A common mistake of context (and generally any -"application" state) is to make it globally available anywhere in your -application when it's actually only needed to be available in a part of the app -(like a single page). Keeping a context value scoped to the area that needs it -most has improved performance and maintainability characteristics. - -## 🦉 Feedback - -Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=03%3A%20useContext%3A%20simple%20Counter&em=). diff --git a/src/exercise/04.md b/src/exercise/04.md deleted file mode 100644 index 7b9feee0..00000000 --- a/src/exercise/04.md +++ /dev/null @@ -1,50 +0,0 @@ -# useLayoutEffect: auto-scrolling textarea - -## 📝 Your Notes - -Elaborate on your learnings here in `src/exercise/04.md` - -## Background - -There are two ways to tell React to run side-effects after it renders: - -1. `useEffect` -2. `useLayoutEffect` - -The difference about these is subtle (they have the exact same API), but -significant. 99% of the time `useEffect` is what you want, but sometimes -`useLayoutEffect` can improve your user experience. - -To learn about the difference, read -[useEffect vs useLayoutEffect](https://kentcdodds.com/blog/useeffect-vs-uselayouteffect) - -And check out the [hook flow diagram](https://github.com/donavon/hook-flow) as -well. - -## Exercise - -Production deploys: - -- [Exercise](https://advanced-react-hooks.netlify.com/isolated/exercise/04.tsx) -- [Final](https://advanced-react-hooks.netlify.com/isolated/final/04.tsx) - -NOTE: React 18 has smoothed out the differences in the UX between `useEffect` -and `useLayoutEffect`. That said, the simple "rule" described still applies! - -There's no exercise for this one because basically you just need to replace -`useEffect` with `useLayoutEffect` and you're good. So you pretty much just need -to experiment with things a bit. - -Before you do that though, compare the finished example with the exercise. -Add/remove messages and you'll find that there's a janky experience with the -exercise version because we're using `useEffect` and there's a gap between the -time that the DOM is visually updated and our code runs. - -Here's the simple rule for when you should use `useLayoutEffect`: If you are -making observable changes to the DOM, then it should happen in -`useLayoutEffect`, otherwise `useEffect`. - -## 🦉 Feedback - -Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=04%3A%20useLayoutEffect%3A%20auto-scrolling%20textarea&em=). diff --git a/src/exercise/06.md b/src/exercise/06.md deleted file mode 100644 index f271cf76..00000000 --- a/src/exercise/06.md +++ /dev/null @@ -1,89 +0,0 @@ -# useDebugValue: useMedia - -## 📝 Your Notes - -Elaborate on your learnings here in `src/exercise/06.md` - -## Background - -[The React DevTools browser extension](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) -is a must-have for any React developer. When you start writing custom hooks, it -can be useful to give them a special label. This is especially useful to -differentiate different usages of the same hook in a given component. - -This is where `useDebugValue` comes in. You use it in a custom hook, and you -call it like so: - -```javascript -function useCount({initialCount = 0, step = 1} = {}) { - React.useDebugValue({initialCount, step}) - const [count, setCount] = React.useState(initialCount) - const increment = () => setCount(c => c + step) - return [count, increment] -} -``` - -So now when people use the `useCount` hook, they'll see the `initialCount` and -`step` values for that particular hook when selecting a component that uses -`useCount` in the DevTools Components tab. - -## Exercise - -Production deploys: - -- [Exercise](https://advanced-react-hooks.netlify.com/isolated/exercise/06.tsx) -- [Final](https://advanced-react-hooks.netlify.com/isolated/final/06.tsx) - -> Note: useDebugValue values will not show in production, because the production build of useDebugValue does nothing. - -In this exercise, we have a custom `useMedia` hook which uses -`window.matchMedia` to determine whether the user-agent satisfies a given media -query. In our `Box` component, we're using it three times to determine whether -the screen is big, medium, or small and we change the color of the box based on -that. - -Now, take a look at the `png` files associated with this exercise -(`src/exercise/06-devtools-before.png` and -`src/exercise/06-devtools-after.png`). You'll notice that the before doesn't -give any useful information for you to know which hook record references which -hook. In the after version, you'll see a really nice label associated with each -hook which makes it obvious which is which. - -If you don't have the browser extension installed, install it now and open the -React tab in the DevTools. Select the `` component in the React tree. -Your job is to use `useDebugValue` to provide a nice label. - -Note: This might be a good one to open in on the isolated page so you can easily -resize the window and because you need to do some fancy stuff to make DevTools -work for the exercise when it's running in an iframe. - -## Extra Credit - -### 1. 💯 use the format function - -[Production deploy](https://advanced-react-hooks.netlify.com/isolated/final/06.extra-1.tsx) - -`useDebugValue` also takes a second argument which is an optional formatter -function, allowing you to do stuff like this if you like: - -```javascript -const formatCountDebugValue = ({initialCount, step}) => - `init: ${initialCount}; step: ${step}` - -function useCount({initialCount = 0, step = 1} = {}) { - React.useDebugValue({initialCount, step}, formatCountDebugValue) - const [count, setCount] = React.useState(0) - const increment = () => setCount(c => c + step) - return [count, increment] -} -``` - -This is only really useful for situations where computing the debug value is -computationally expensive (and therefore you only want it calculated when the -DevTools are open and not when your users are using the app). In our case this -is not necessary, however, go ahead and give it a try anyway. - -## 🦉 Feedback - -Fill out -[the feedback form](https://ws.kcd.im/?ws=Advanced%20React%20Hooks%20%F0%9F%94%A5&e=06%3A%20useDebugValue%3A%20useMedia&em=). diff --git a/src/final/02.extra-2.tsx b/src/final/02.extra-2.tsx deleted file mode 100644 index 9d91ab49..00000000 --- a/src/final/02.extra-2.tsx +++ /dev/null @@ -1,143 +0,0 @@ -// useCallback: custom hooks -// 💯 return a memoized `run` function from useAsync -// http://localhost:3000/isolated/final/02.extra-2.tsx - -import * as React from 'react' -import { - fetchPokemon, - PokemonForm, - PokemonDataView, - PokemonInfoFallback, - PokemonErrorBoundary, -} from '../pokemon' -import {PokemonData} from '../types' - -type AsyncState = - | { - status: 'idle' - data?: null - error?: null - } - | { - status: 'pending' - data?: null - error?: null - } - | { - status: 'resolved' - data: DataType - error: null - } - | { - status: 'rejected' - data: null - error: Error - } - -type AsyncAction = - | {type: 'reset'} - | {type: 'pending'} - | {type: 'resolved'; data: DataType} - | {type: 'rejected'; error: Error} - -function asyncReducer( - state: AsyncState, - action: AsyncAction, -): AsyncState { - switch (action.type) { - case 'pending': { - return {status: 'pending', data: null, error: null} - } - case 'resolved': { - return {status: 'resolved', data: action.data, error: null} - } - case 'rejected': { - return {status: 'rejected', data: null, error: action.error} - } - default: { - throw new Error(`Unhandled action type: ${action.type}`) - } - } -} - -function useAsync() { - const [state, dispatch] = React.useReducer< - React.Reducer, AsyncAction> - >(asyncReducer, { - status: 'idle', - data: null, - error: null, - }) - - const {data, error, status} = state - - const run = React.useCallback((promise: Promise) => { - dispatch({type: 'pending'}) - promise.then( - data => { - dispatch({type: 'resolved', data}) - }, - error => { - dispatch({type: 'rejected', error}) - }, - ) - }, []) - - return { - error, - status, - data, - run, - } -} - -function PokemonInfo({pokemonName}: {pokemonName: string}) { - const {data: pokemon, status, error, run} = useAsync() - - React.useEffect(() => { - if (!pokemonName) { - return - } - const pokemonPromise = fetchPokemon(pokemonName) - run(pokemonPromise) - }, [pokemonName, run]) - - switch (status) { - case 'idle': - return Submit a pokemon - case 'pending': - return - case 'rejected': - throw error - case 'resolved': - return - default: - throw new Error('This should be impossible') - } -} - -function App() { - const [pokemonName, setPokemonName] = React.useState('') - - function handleSubmit(newPokemonName: string) { - setPokemonName(newPokemonName) - } - - function handleReset() { - setPokemonName('') - } - - return ( -
- -
-
- - - -
-
- ) -} - -export default App diff --git a/src/final/02.extra-3.js b/src/final/02.extra-3.js deleted file mode 100644 index eec5a751..00000000 --- a/src/final/02.extra-3.js +++ /dev/null @@ -1,160 +0,0 @@ -// useCallback: custom hooks -// 💯 make safeDispatch with useCallback, useRef, and useEffect -// http://localhost:3000/isolated/final/02.extra-3.js - -import * as React from 'react' -import { - fetchPokemon, - PokemonForm, - PokemonDataView, - PokemonInfoFallback, - PokemonErrorBoundary, -} from '../pokemon' - -function useSafeDispatch(dispatch) { - const mountedRef = React.useRef(false) - - // to make this even more generic you should use the useLayoutEffect hook to - // make sure that you are correctly setting the mountedRef.current immediately - // after React updates the DOM. Even though this effect does not interact - // with the dom another side effect inside a useLayoutEffect which does - // interact with the dom may depend on the value being set - React.useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - } - }, []) - - return React.useCallback( - (...args) => (mountedRef.current ? dispatch(...args) : void 0), - [dispatch], - ) -} - -function asyncReducer(state, action) { - switch (action.type) { - case 'pending': { - return {status: 'pending', data: null, error: null} - } - case 'resolved': { - return {status: 'resolved', data: action.data, error: null} - } - case 'rejected': { - return {status: 'rejected', data: null, error: action.error} - } - default: { - throw new Error(`Unhandled action type: ${action.type}`) - } - } -} - -function useAsync(initialState) { - const [state, unsafeDispatch] = React.useReducer(asyncReducer, { - status: 'idle', - data: null, - error: null, - ...initialState, - }) - - const dispatch = useSafeDispatch(unsafeDispatch) - - const {data, error, status} = state - - const run = React.useCallback( - promise => { - dispatch({type: 'pending'}) - promise.then( - data => { - dispatch({type: 'resolved', data}) - }, - error => { - dispatch({type: 'rejected', error}) - }, - ) - }, - [dispatch], - ) - - return { - error, - status, - data, - run, - } -} - -function PokemonInfo({pokemonName}) { - const { - data: pokemon, - status, - error, - run, - } = useAsync({ - status: pokemonName ? 'pending' : 'idle', - }) - - React.useEffect(() => { - if (!pokemonName) { - return - } - run(fetchPokemon(pokemonName)) - }, [pokemonName, run]) - - switch (status) { - case 'idle': - return Submit a pokemon - case 'pending': - return - case 'rejected': - throw error - case 'resolved': - return - default: - throw new Error('This should be impossible') - } -} - -function App() { - const [pokemonName, setPokemonName] = React.useState('') - - function handleSubmit(newPokemonName) { - setPokemonName(newPokemonName) - } - - function handleReset() { - setPokemonName('') - } - - return ( -
- -
-
- - - -
-
- ) -} - -function AppWithUnmountCheckbox() { - const [mountApp, setMountApp] = React.useState(true) - return ( -
- -
- {mountApp ? : null} -
- ) -} - -export default AppWithUnmountCheckbox