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
-```
-
-`` 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 (
-