├── .all-contributorsrc
├── .env
├── .eslintignore
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── OUTLINE.md
├── README.md
├── appveyor.yml
├── jsconfig.json
├── package.json
├── public
├── _headers
├── _redirects
├── antic-slab.woff2
├── img
│ ├── pokeball.png
│ ├── pokemon-cafe.jpg
│ ├── pokemon
│ │ ├── bulbasaur.jpg
│ │ ├── charizard.jpg
│ │ ├── ditto.jpg
│ │ ├── fallback-pokemon.jpg
│ │ ├── mew.jpg
│ │ ├── mewtwo.jpg
│ │ └── pikachu.jpg
│ ├── pokemongo.jpg
│ └── squirtle-toy.jpg
├── index.html
├── manifest.json
└── serve.json
├── sandbox.config.json
├── scripts
├── autofill-feedback-email.js
├── setup.js
└── workshop-setup.js
├── src
├── .eslintrc
├── @types
│ └── global.d.ts
├── __tests__
│ └── 01.js
├── app.js
├── examples
│ ├── fetch-approaches
│ │ ├── fetch-on-render.js
│ │ ├── fetch-then-render.js
│ │ ├── lazy
│ │ │ ├── pokemon-info-fetch-on-render.js
│ │ │ ├── pokemon-info-fetch-then-render.js
│ │ │ ├── pokemon-info-render-as-you-fetch.data.js
│ │ │ └── pokemon-info-render-as-you-fetch.js
│ │ └── render-as-you-fetch.js
│ └── preload-image.js
├── exercises-final
│ ├── 01-extra.1.js
│ ├── 01-extra.2.js
│ ├── 01-extra.3.js
│ ├── 01.js
│ ├── 02-extra.1.js
│ ├── 02.js
│ ├── 03-extra.1.js
│ ├── 03-extra.2.js
│ ├── 03.js
│ ├── 04.js
│ ├── 05-extra.1.js
│ ├── 05.js
│ ├── 06.js
│ ├── 07-extra.1.js
│ └── 07.js
├── exercises
│ ├── 01.js
│ ├── 01.md
│ ├── 02.js
│ ├── 02.md
│ ├── 03.js
│ ├── 03.md
│ ├── 04.js
│ ├── 04.md
│ ├── 05.js
│ ├── 05.md
│ ├── 06.js
│ ├── 06.md
│ ├── 07.js
│ └── 07.md
├── fetch-pokemon.js
├── hacks
│ ├── fetch.js
│ ├── index.js
│ ├── pokemon.json
│ ├── transactions.json
│ └── users.json
├── index.js
├── load-exercises.js
├── setupTests.js
├── styles.css
├── suspense-list
│ ├── app.module.css
│ ├── img.js
│ ├── left-nav.js
│ ├── left-nav.module.css
│ ├── main-content.js
│ ├── main-content.module.css
│ ├── nav-bar.js
│ ├── nav-bar.module.css
│ ├── right-nav.js
│ ├── right-nav.module.css
│ ├── spinner.js
│ ├── spinner.module.css
│ └── style-overrides.css
└── utils.js
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "concurrent-react",
3 | "projectOwner": "kentcdodds",
4 | "repoType": "github",
5 | "files": [
6 | "README.md"
7 | ],
8 | "imageSize": 100,
9 | "commit": false,
10 | "contributors": [
11 | {
12 | "login": "kentcdodds",
13 | "name": "Kent C. Dodds",
14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
15 | "profile": "https://kentcdodds.com",
16 | "contributions": [
17 | "code",
18 | "doc",
19 | "infra",
20 | "test"
21 | ]
22 | }
23 | ],
24 | "repoHost": "https://github.com",
25 | "contributorsPerLine": 7
26 | }
27 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | build
4 | scripts/workshop-setup.js
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | build
4 | .idea/
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | build
4 | scripts/workshop-setup.js
5 | src/babel.js
6 | src/react-dom.development.js
7 | src/react.development.js
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": false,
9 | "jsxBracketSameLine": false,
10 | "proseWrap": "always"
11 | }
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js: stable
4 | install: echo "Installation happens in the setup script"
5 | cache:
6 | directories:
7 | - node_modules
8 | notifications:
9 | email: false
10 | branches:
11 | only:
12 | - master
13 | script:
14 | - npm run setup
15 | after_success:
16 | - npx codecov
17 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of
9 | experience, nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or reject
41 | comments, commits, code, wiki edits, issues, and other contributions that are
42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any
43 | contributor for other behaviors that they deem inappropriate, threatening,
44 | offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at kent@doddsfamily.us. All complaints
59 | will be reviewed and investigated and will result in a response that is deemed
60 | necessary and appropriate to the circumstances. The project team is obligated to
61 | maintain confidentiality with regard to the reporter of an incident. Further
62 | details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | **Working on your first Pull Request?** You can learn how from this _free_
6 | series [How to Contribute to an Open Source Project on GitHub][egghead]
7 |
8 | ## Project setup
9 |
10 | 1. Fork and clone the repo
11 | 2. Run `npm run setup -s` to install dependencies and run validation
12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name`
13 |
14 | > Tip: Keep your `master` branch pointing at the original repository and make
15 | > pull requests from branches on your fork. To do this, run:
16 | >
17 | > ```
18 | > git remote add upstream https://github.com/kentcdodds/concurrent-react.git
19 | > git fetch upstream
20 | > git branch --set-upstream-to=upstream/master master
21 | > ```
22 | >
23 | > This will add the original repository as a "remote" called "upstream," Then
24 | > fetch the git information from that remote, then set your local `master`
25 | > branch to use the upstream master branch whenever you run `git pull`. Then you
26 | > can make all of your pull request branches based on this `master` branch.
27 | > Whenever you want to update your version of `master`, do a regular `git pull`.
28 |
29 | ## Committing and Pushing changes
30 |
31 | Please make sure to run the tests before you commit your changes. You can run
32 | `npm run test` and press `u` which will update any snapshots that need updating.
33 | Make sure to include those changes (if they exist) in your commit.
34 |
35 | ## Help needed
36 |
37 | Please checkout the [the open issues][issues]
38 |
39 | Also, please watch the repo and respond to questions/bug reports/feature
40 | requests! Thanks!
41 |
42 | [egghead]:
43 | https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
44 | [issues]: https://github.com/kentcdodds/concurrent-react/issues
45 |
--------------------------------------------------------------------------------
/OUTLINE.md:
--------------------------------------------------------------------------------
1 | # Concurrent React
2 |
3 | > Concurrent React Workshop 🔀
4 |
5 | 👋 I'm Kent C. Dodds
6 |
7 | - 🏡 Utah
8 | - 👩 👧 👦 👦 👦 🐕
9 | - 🏢 kentcdodds.com
10 | - 🐦/🐙 @kentcdodds
11 | - 🏆 testingjavascript.com
12 | - 🥚 kcd.im/egghead
13 | - 🥋 kcd.im/fem
14 | - 💌 kcd.im/news
15 | - 📝 kcd.im/blog
16 | - 📺 kcd.im/devtips
17 | - 💻 kcd.im/coding
18 | - 📽 kcd.im/youtube
19 | - 🎙 kcd.im/3-mins
20 | - ❓ kcd.im/ama
21 |
22 | # What this workshop is
23 |
24 | 1. Exercises to prepare your brain to learn
25 | 2. Instruction for you to ask questions
26 |
27 | # What this workshop is not
28 |
29 | - Solo
30 | - Lecture
31 | - One day (do it all again next week)
32 |
33 | # Logistics
34 |
35 | ## Schedule
36 |
37 | - 😴 Logistics
38 | - 🏋 Simple Data-fetching
39 | - 😴 10 Minutes
40 | - 🏋 Render as you fetch
41 | - 🏋 useTransition for improved loading states
42 | - 😴 30 Minutes
43 | - 🏋 Suspense Image
44 | - 🏋 Cache resources
45 | - 😴 10 Minutes
46 | - 🏋 Suspense with a custom hook
47 | - 🏋 Coordinate Suspending components with SuspenseList
48 | - ❓ Q&A
49 |
50 | ## Scripts
51 |
52 | - `npm run start`
53 | - `npm run test` (not ready yet).
54 |
55 | ## Asking Questions
56 |
57 | Please do ask! Interrupt me. If you have an unrelated question, please ask on
58 | [my AMA](https://kcd.im/ama).
59 |
60 | ## Zoom
61 |
62 | - Help us make this more human by keeping your video on if possible
63 | - Keep microphone muted unless speaking
64 | - Breakout rooms
65 |
66 | ## Exercises
67 |
68 | - `src/exercises/0x.md`: Background, Exercise Instructions, Extra Credit
69 | - `src/exercises/0x.js`: Exercise with Emoji helpers
70 | - `src/__tests__/0x.js`: Tests
71 | - `src/exercises-final/0x.js`: Final version
72 |
73 | > NOTE: Some of the extra credit have tests that are specific to their
74 | > implementation because the implementation is significantly different and your
75 | > work needs to be checked differently.
76 |
77 | ## Emoji
78 |
79 | - **Kody the Koala Bear** 🐨 "Do this"
80 | - **Marty the Money Bag** 💰 "Here's a hint"
81 | - **Hannah the Hundred** 💯 "Extra Credit"
82 | - **Olivia the Owl** 🦉 "Pro-tip"
83 | - **Dominic the Document** 📜 "Docs links"
84 | - **Berry the Bomb** 💣 "Remove this code"
85 | - **Peter the Product Manager** 👨💼 "Story time"
86 | - **Alfred the Alert** 🚨 "Extra helpful in test errors"
87 |
88 | ## Disclaimers
89 |
90 | 1. React Concurrent Mode is experimental
91 | 2. I've never shipped Concurrent Mode to production, and you shouldn't either
92 | (yet)
93 | 3. React Suspense is a particularly primitive API and we're still working out
94 | good abstractions for it
95 | 4. I've been informed that the API for suspending _will change_ before the
96 | stable release (don't worry though, the concepts are solid)
97 | 5. I will probably say "I don't know" as a response to your questions sometimes
98 |
99 | ## Workshop Feedback
100 |
101 | Each exercise has an Elaboration and Feedback link. Please fill that out after
102 | the exercise and instruction.
103 |
104 | At the end of the workshop, please go to this URL to give overall feedback.
105 | Thank you! https://kcd.im/rs-ws-feedback
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Concurrent React
2 |
3 | > Improving UX with a faster, more predictable app.
4 |
5 | > A NEW version of this project is available at:
6 | > https://github.com/kentcdodds/react-suspense
7 |
8 | 👋 hi there! My name is [Kent C. Dodds](https://kentcdodds.com)! This is a
9 | workshop repo to teach you the fundamentals of React's (EXPERIMENTAL)
10 | [concurrent mode](https://reactjs.org/concurrent). This feature enables React to
11 | make your app faster out of the box and it comes along with a few features that
12 | you can use to improve your app's user experience (most notably the concept of
13 | "Suspense").
14 |
15 | [![Build Status][build-badge]][build]
16 | [![AppVeyor Build Status][win-build-badge]][win-build]
17 | [![Code Coverage][coverage-badge]][coverage]
18 | [![GPL 3.0 License][license-badge]][license]
19 | [](#contributors-)
20 | [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc]
21 |
22 | ## ⚠️ Warning ⚠️
23 |
24 | This workshop material deals with **EXPERIMENTAL** features in React. Please do
25 | not copy/paste any of the code you find here into a production application and
26 | expect it to work. Even when the features are released they may not work the
27 | same as demonstrated in this workshop material.
28 |
29 | That said, the concepts in this workshop will very likely be applicable when
30 | these features are stable, so enjoy the workshop!
31 |
32 | ## Pre-Workshop Instructions/Requirements
33 |
34 | In order for us to maximize our efforts during the workshop, please do the
35 | following:
36 |
37 | - [ ] Setup the project (follow the setup instructions below) (~5 minutes)
38 | - [ ] Install and setup [Zoom](https://zoom.us) on the computer you will be
39 | using (~5 minutes)
40 | - [ ] Install the React DevTools
41 | ([Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
42 | (recommended),
43 | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/))
44 | - [ ] Watch
45 | [Use Zoom for KCD Workshops](https://egghead.io/lessons/egghead-use-zoom-for-kcd-workshops)
46 | (~8 minutes).
47 | - [ ] Watch
48 | [Setup and Logistics for KCD Workshops](https://egghead.io/lessons/egghead-setup-and-logistics-for-kcd-workshops)
49 | (~24 minutes). Please do NOT skip this step.
50 | - [ ] Watch Dan Abramov's talk
51 | [Beyond React 16 | JSConf Iceland 2018](https://www.youtube.com/watch?v=nLF0n9SACd4)
52 | (33 minutes)
53 | - [ ] Go through my
54 | [Learn React Hooks Workshop](https://kentcdodds.com/workshops/hooks), or
55 | have the equivalent basic experience of using hooks. You should be
56 | experienced with `useState`, `useEffect`, and `useRef`.
57 | - [ ] Go through my
58 | [Advanced React Hooks Workshop](https://kentcdodds.com/workshops/advanced-react-hooks),
59 | or have the equivalent experience. You should be experienced with
60 | `useContext` and `useReducer` (experience with `useMemo` and `useCallback`
61 | is a bonus).
62 |
63 | The more prepared you are for the workshop, the better it will go for you.
64 |
65 | ## Workshop Outline
66 |
67 | Here are the concepts we'll be covering:
68 |
69 | - Opting into React Concurrent Mode
70 | - Thinking in Suspense
71 | - The fundamentals of "suspending"
72 | - Structuring `` components with fallbacks
73 | - Using `useTransition`
74 | - Refactor an existing async interaction to suspense
75 | - The difference between the three data-fetching approaches:
76 | - Fetch-on-Render (not using Suspense)
77 | - Fetch-Then-Render (not using Suspense)
78 | - Render-as-You-Fetch (using Suspense)
79 | - Using `` to coordinate multiple suspending components
80 |
81 | ## System Requirements
82 |
83 | - [git][git] v2 or greater
84 | - [NodeJS][node] v8 or greater
85 | - [yarn][yarn] v1 or greater (or [npm][npm] v6 or greater)
86 |
87 | All of these must be available in your `PATH`. To verify things are set up
88 | properly, you can run this:
89 |
90 | ```shell
91 | git --version
92 | node --version
93 | yarn --version # or npm --version
94 | ```
95 |
96 | If you have trouble with any of these, learn more about the PATH environment
97 | variable and how to fix it here for [windows][win-path] or
98 | [mac/linux][mac-path].
99 |
100 | ## Setup
101 |
102 | For many of my workshops, you should be able to run them
103 | [entirely in the browser](https://codesandbox.io/s/github/kentcdodds/concurrent-react).
104 | However for this one, I recommend you work through the workshop on your own
105 | computer.
106 |
107 | To do so, please follow these instructions.
108 |
109 | After you've made sure to have the correct things (and versions) installed (as
110 | indicated above), you should be able to just run a few commands to get set up:
111 |
112 | ```
113 | git clone https://github.com/kentcdodds/concurrent-react.git
114 | cd concurrent-react
115 | npm run setup --silent
116 | ```
117 |
118 | This may take a few minutes. **It will ask you for your email.** This is
119 | optional and just automatically adds your email to the links in the project to
120 | make filling out some forms easier If you get any errors, please read through
121 | them and see if you can find out what the problem is. You may also want to look
122 | at [Troubleshooting](#troubleshooting). If you can't work it out on your own
123 | then please [file an issue][issue] and provide _all_ the output from the
124 | commands you ran (even if it's a lot).
125 |
126 | ## Running the app
127 |
128 | To get the app up and running (and really see if it worked), run:
129 |
130 | ```shell
131 | npm start
132 | ```
133 |
134 | This should start up your browser. If you're familiar, this is a standard
135 | [react-scripts](https://github.com/facebook/create-react-app) application.
136 |
137 | You can also open
138 | [the deployment of the app on Netlify](https://concurrent-react.netlify.com/).
139 |
140 | ## Running the tests
141 |
142 | ```shell
143 | npm test
144 | ```
145 |
146 | This will start [Jest](http://facebook.github.io/jest) in watch mode. Read the
147 | output and play around with it.
148 |
149 | **Your goal will be to go into each test, swap the final version for the
150 | exercise version in the import, and make the tests pass**
151 |
152 | ## Helpful Emoji 🐨 💰 💯 🦉 📜 💣 🚨
153 |
154 | Each exercise has comments in it to help you get through the exercise. These fun
155 | emoji characters are here to help you.
156 |
157 | - **Kody the Koala Bear** 🐨 will tell you when there's something specific you
158 | should do
159 | - **Marty the Money Bag** 💰 will give you specific tips (and sometimes code)
160 | along the way
161 | - **Hannah the Hundred** 💯 will give you extra challenges you can do if you
162 | finish the exercises early.
163 | - **Olivia the Owl** 🦉 will give you useful tidbits/best practice notes and a
164 | link for elaboration and feedback.
165 | - **Dominic the Document** 📜 will give you links to useful documentation
166 | - **Berry the Bomb** 💣 will be hanging around anywhere you need to blow stuff
167 | up (delete code)
168 | - **Peter the Product Manager** 👨💼 helps us know what our users want
169 | - **Alfred the Alert** 🚨 will occasionally show up in the test failures with
170 | potential explanations for why the tests are failing.
171 |
172 | ## Troubleshooting
173 |
174 |
175 |
176 | "npm run setup" command not working
177 |
178 | Here's what the setup script does. If it fails, try doing each of these things
179 | individually yourself:
180 |
181 | ```
182 | # verify your environment will work with the project
183 | node ./scripts/verify
184 |
185 | # install dependencies
186 | npm install
187 |
188 | # verify the project is ready to run
189 | npm run build
190 | npm run test:coverage
191 | npm run lint
192 |
193 | # automatically fill in your email for the feedback links.
194 | node ./scripts/autofill-feedback-email.js
195 | ```
196 |
197 | If any of those scripts fail, please try to work out what went wrong by the
198 | error message you get. If you still can't work it out, feel free to [open an
199 | issue][issue] with _all_ the output from that script. I will try to help if I
200 | can.
201 |
202 |
203 |
204 | ## Contributors
205 |
206 | Thanks goes to these wonderful people
207 | ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
208 |
209 |
210 |
211 |
212 |
38 | )
39 | }
40 |
41 | export default PreloadImageExample
42 |
--------------------------------------------------------------------------------
/src/exercises-final/01-extra.1.js:
--------------------------------------------------------------------------------
1 | // Simple Data-fetching
2 | // 💯 add error handling with an Error Boundary
3 |
4 | // http://localhost:3000/isolated/exercises-final/01-extra.1
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {ErrorBoundary, PokemonDataView} from '../utils'
9 |
10 | // By default, all fetches are mocked so we can control the time easily.
11 | // You can adjust the fetch time with this:
12 | // window.FETCH_TIME = 3000
13 | // If you want to make an actual network call for the pokemon
14 | // then uncomment the following line
15 | // window.fetch.restoreOriginalFetch()
16 | // Note that by doing this, the FETCH_TIME will no longer be considered
17 | // and if you want to slow things down you should use the Network tab
18 | // in your developer tools to throttle your network to something like "Slow 3G"
19 |
20 | let pokemon
21 | let pokemonError
22 | let pokemonPromise = fetchPokemon('pikachu').then(
23 | p => (pokemon = p),
24 | e => (pokemonError = e),
25 | )
26 |
27 | function PokemonInfo() {
28 | if (pokemonError) {
29 | throw pokemonError
30 | }
31 | if (!pokemon) {
32 | throw pokemonPromise
33 | }
34 | return (
35 |
}>
49 |
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | export default App
57 |
--------------------------------------------------------------------------------
/src/exercises-final/01-extra.2.js:
--------------------------------------------------------------------------------
1 | // Simple Data-fetching
2 | // 💯 make more generic createResource
3 |
4 | // http://localhost:3000/isolated/exercises-final/01-extra.2
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {ErrorBoundary, PokemonDataView} from '../utils'
9 |
10 | // By default, all fetches are mocked so we can control the time easily.
11 | // You can adjust the fetch time with this:
12 | // window.FETCH_TIME = 3000
13 | // If you want to make an actual network call for the pokemon
14 | // then uncomment the following line
15 | // window.fetch.restoreOriginalFetch()
16 | // Note that by doing this, the FETCH_TIME will no longer be considered
17 | // and if you want to slow things down you should use the Network tab
18 | // in your developer tools to throttle your network to something like "Slow 3G"
19 |
20 | let pokemonResource = createResource(() => fetchPokemon('pikachu'))
21 |
22 | function createResource(asyncFn) {
23 | let status = 'pending'
24 | let result
25 | let promise = asyncFn().then(
26 | r => {
27 | status = 'success'
28 | result = r
29 | },
30 | e => {
31 | status = 'error'
32 | result = e
33 | },
34 | )
35 | return {
36 | read() {
37 | if (status === 'pending') throw promise
38 | if (status === 'error') throw result
39 | if (status === 'success') return result
40 | throw new Error('This should be impossible')
41 | },
42 | }
43 | }
44 |
45 | function Pokemon() {
46 | const pokemon = pokemonResource.read()
47 | return (
48 |
}>
62 |
63 |
64 |
65 |
66 | )
67 | }
68 |
69 | export default App
70 |
--------------------------------------------------------------------------------
/src/exercises-final/01-extra.3.js:
--------------------------------------------------------------------------------
1 | // Simple Data-fetching
2 | // 💯 Use utils
3 |
4 | // http://localhost:3000/isolated/exercises-final/01-extra.3
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {
9 | ErrorBoundary,
10 | PokemonInfoFallback,
11 | createResource,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | let pokemonResource = createResource(() => fetchPokemon('pikachu'))
26 |
27 | function Pokemon() {
28 | const pokemon = pokemonResource.read()
29 | return (
30 |
48 | )
49 | }
50 |
51 | export default App
52 |
--------------------------------------------------------------------------------
/src/exercises-final/01.js:
--------------------------------------------------------------------------------
1 | // Simple Data-fetching
2 |
3 | // http://localhost:3000/isolated/exercises-final/01
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {PokemonDataView} from '../utils'
8 |
9 | // By default, all fetches are mocked so we can control the time easily.
10 | // You can adjust the fetch time with this:
11 | // window.FETCH_TIME = 3000
12 | // If you want to make an actual network call for the pokemon
13 | // then uncomment the following line
14 | // window.fetch.restoreOriginalFetch()
15 | // Note that by doing this, the FETCH_TIME will no longer be considered
16 | // and if you want to slow things down you should use the Network tab
17 | // in your developer tools to throttle your network to something like "Slow 3G"
18 |
19 | let pokemon
20 | let pokemonPromise = fetchPokemon('pikachu').then(p => (pokemon = p))
21 |
22 | function PokemonInfo() {
23 | if (!pokemon) {
24 | throw pokemonPromise
25 | }
26 | return (
27 |
}>
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default App
47 |
--------------------------------------------------------------------------------
/src/exercises-final/02-extra.1.js:
--------------------------------------------------------------------------------
1 | // Refactor useEffect to Suspense
2 | // 💯 Suspense and Error Boundary positioning
3 |
4 | // http://localhost:3000/isolated/exercises-final/02-extra.1
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {
9 | ErrorBoundary,
10 | createResource,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | function PokemonInfo({pokemonResource}) {
27 | const pokemon = pokemonResource.read()
28 | return (
29 |
71 |
72 |
73 | )
74 | }
75 |
76 | export default App
77 |
--------------------------------------------------------------------------------
/src/exercises-final/02.js:
--------------------------------------------------------------------------------
1 | // Refactor useEffect to Suspense
2 |
3 | // http://localhost:3000/isolated/exercises-final/02
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | function PokemonInfo({pokemonResource}) {
26 | const pokemon = pokemonResource.read()
27 | return (
28 |
68 | )
69 | }
70 |
71 | export default App
72 |
--------------------------------------------------------------------------------
/src/exercises-final/03-extra.1.js:
--------------------------------------------------------------------------------
1 | // useTransition for improved loading states
2 | // 💯 use css transitions
3 |
4 | // http://localhost:3000/isolated/exercises-final/03-extra.1
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {
9 | ErrorBoundary,
10 | createResource,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | function PokemonInfo({pokemonResource}) {
27 | const pokemon = pokemonResource.read()
28 | return (
29 |
83 | )
84 | }
85 |
86 | export default App
87 |
--------------------------------------------------------------------------------
/src/exercises-final/03-extra.2.js:
--------------------------------------------------------------------------------
1 | // useTransition for improved loading states
2 | // 💯 avoid flash of loading content
3 |
4 | // http://localhost:3000/isolated/exercises-final/03-extra.2
5 |
6 | import React from 'react'
7 | import fetchPokemon from '../fetch-pokemon'
8 | import {
9 | ErrorBoundary,
10 | createResource,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | function PokemonInfo({pokemonResource}) {
27 | const pokemon = pokemonResource.read()
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const SUSPENSE_CONFIG = {
39 | timeoutMs: 4000,
40 | busyDelayMs: 300, // this time is slightly shorter than our css transition delay
41 | busyMinDurationMs: 700,
42 | }
43 |
44 | function createPokemonResource(pokemonName) {
45 | // fetchPokemon takes an optional second argument called "delay" which
46 | // allows you to arbitrarily delay the fetch request by a given number
47 | // of milliseconds. For example:
48 | // fetchPokemon(pokemonName, 400)
49 | // would delay it to at least take 400 milliseconds
50 | return createResource(() => fetchPokemon(pokemonName))
51 | }
52 |
53 | function App() {
54 | const [pokemonName, setPokemonName] = React.useState('')
55 | const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
56 | const [pokemonResource, setPokemonResource] = React.useState(null)
57 |
58 | function handleSubmit(newPokemonName) {
59 | setPokemonName(newPokemonName)
60 | startTransition(() => {
61 | setPokemonResource(createPokemonResource(newPokemonName))
62 | })
63 | }
64 |
65 | return (
66 |
83 | )
84 | }
85 |
86 | export default App
87 |
--------------------------------------------------------------------------------
/src/exercises-final/03.js:
--------------------------------------------------------------------------------
1 | // useTransition for improved loading states
2 |
3 | // http://localhost:3000/isolated/exercises-final/03
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | function PokemonInfo({pokemonResource}) {
26 | const pokemon = pokemonResource.read()
27 | return (
28 |
84 | )
85 | }
86 |
87 | export default App
88 |
--------------------------------------------------------------------------------
/src/exercises-final/04.js:
--------------------------------------------------------------------------------
1 | // Cache resources
2 |
3 | // http://localhost:3000/isolated/exercises-final/04
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | function PokemonInfo({pokemonResource}) {
26 | const pokemon = pokemonResource.read()
27 | return (
28 |
89 | )
90 | }
91 |
92 | export default App
93 |
--------------------------------------------------------------------------------
/src/exercises-final/05-extra.1.js:
--------------------------------------------------------------------------------
1 | // Suspense Image
2 | // 💯 avoid waterfall
3 |
4 | // http://localhost:3000/isolated/exercises-final/05-extra.1
5 |
6 | import React from 'react'
7 | import fetchPokemon, {getImageUrlForPokemon} from '../fetch-pokemon'
8 | import {
9 | ErrorBoundary,
10 | createResource,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | // 🦉 On this one, make sure that you uncheck the "Disable cache" checkbox.
27 | // We're relying on that cache for this approach to work!
28 |
29 | function preloadImage(src) {
30 | return new Promise(resolve => {
31 | const img = document.createElement('img')
32 | img.src = src
33 | img.onload = () => resolve(src)
34 | })
35 | }
36 |
37 | function PokemonInfo({pokemonResource}) {
38 | const pokemon = pokemonResource.data.read()
39 | return (
40 |
106 | )
107 | }
108 |
109 | export default App
110 |
--------------------------------------------------------------------------------
/src/exercises-final/05.js:
--------------------------------------------------------------------------------
1 | // Suspense Image
2 |
3 | // http://localhost:3000/isolated/exercises-final/05
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | // 🦉 On this one, make sure that you uncheck the "Disable cache" checkbox.
26 | // We're relying on that cache for this approach to work!
27 |
28 | function preloadImage(src) {
29 | return new Promise(resolve => {
30 | const img = document.createElement('img')
31 | img.src = src
32 | img.onload = () => resolve(src)
33 | })
34 | }
35 |
36 | const imgSrcResourceCache = {}
37 |
38 | function Img({src, alt, ...props}) {
39 | let imgSrcResource = imgSrcResourceCache[src]
40 | if (!imgSrcResource) {
41 | imgSrcResource = createResource(() => preloadImage(src))
42 | imgSrcResourceCache[src] = imgSrcResource
43 | }
44 | return
45 | }
46 |
47 | function PokemonInfo({pokemonResource}) {
48 | const pokemon = pokemonResource.read()
49 | return (
50 |
111 | )
112 | }
113 |
114 | export default App
115 |
--------------------------------------------------------------------------------
/src/exercises-final/06.js:
--------------------------------------------------------------------------------
1 | // Suspense with a custom hook
2 |
3 | // http://localhost:3000/isolated/exercises-final/06
4 |
5 | import React from 'react'
6 | import fetchPokemon, {getImageUrlForPokemon} from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | preloadImage,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | function PokemonInfo({pokemonResource}) {
27 | const pokemon = pokemonResource.data.read()
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const SUSPENSE_CONFIG = {
39 | timeoutMs: 4000,
40 | busyDelayMs: 300, // this time is slightly shorter than our css transition delay
41 | busyMinDurationMs: 700,
42 | }
43 |
44 | const pokemonResourceCache = {}
45 |
46 | function getPokemonResource(name) {
47 | const lowerName = name.toLowerCase()
48 | let resource = pokemonResourceCache[lowerName]
49 | if (!resource) {
50 | resource = createPokemonResource(lowerName)
51 | pokemonResourceCache[lowerName] = resource
52 | }
53 | return resource
54 | }
55 |
56 | function createPokemonResource(pokemonName) {
57 | const lowerName = pokemonName
58 | const data = createResource(() => fetchPokemon(lowerName))
59 | const image = createResource(() =>
60 | preloadImage(getImageUrlForPokemon(lowerName)),
61 | )
62 | return {data, image}
63 | }
64 |
65 | function usePokemonResource(pokemonName) {
66 | const [pokemonResource, setPokemonResource] = React.useState(null)
67 | const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
68 |
69 | React.useLayoutEffect(() => {
70 | if (!pokemonName) {
71 | return
72 | }
73 | startTransition(() => {
74 | setPokemonResource(getPokemonResource(pokemonName))
75 | })
76 |
77 | // ESLint wants me to add startTransition to the dependency list. I'm
78 | // excluding it like we are because of a known bug which will be fixed
79 | // before the stable release of Concurrent React:
80 | // https://github.com/facebook/react/issues/17273
81 | // eslint-disable-next-line react-hooks/exhaustive-deps
82 | }, [pokemonName])
83 |
84 | return [pokemonResource, isPending]
85 | }
86 |
87 | function App() {
88 | const [pokemonName, setPokemonName] = React.useState('')
89 |
90 | const [pokemonResource, isPending] = usePokemonResource(pokemonName)
91 |
92 | function handleSubmit(newPokemonName) {
93 | setPokemonName(newPokemonName)
94 | }
95 |
96 | return (
97 |
79 | )
80 | }
81 |
82 | export default App
83 |
--------------------------------------------------------------------------------
/src/exercises/01.js:
--------------------------------------------------------------------------------
1 | // Simple Data-fetching
2 |
3 | // http://localhost:3000/isolated/exercises/01
4 |
5 | import React from 'react'
6 | import {PokemonDataView} from '../utils'
7 | // 🐨 you'll need to import the fetchPokemon function
8 | // 💰 here you go:
9 | // import fetchPokemon from '../fetch-pokemon'
10 | // 💰 use it like this: fetchPokemon(pokemonName).then(handleSuccess, handleFailure)
11 |
12 | // you'll also need the ErrorBoundary component from utils
13 | // 💰 here you go:
14 | // import {ErrorBoundary} from '../utils'
15 | // 💰 use it like this:
16 |
17 | // By default, all fetches are mocked so we can control the time easily.
18 | // You can adjust the fetch time with this:
19 | // window.FETCH_TIME = 3000
20 | // If you want to make an actual network call for the pokemon
21 | // then uncomment the following line
22 | // window.fetch.restoreOriginalFetch()
23 | // Note that by doing this, the FETCH_TIME will no longer be considered
24 | // and if you want to slow things down you should use the Network tab
25 | // in your developer tools to throttle your network to something like "Slow 3G"
26 |
27 | // 🐨 create the following mutable variable references (using let):
28 | // pokemon, pokemonError, pokemonPromise
29 |
30 | // 💣 delete this now...
31 | const pokemon = {
32 | name: 'TODO',
33 | number: 'TODO',
34 | attacks: {
35 | special: [{name: 'TODO', type: 'TODO', damage: 'TODO'}],
36 | },
37 | fetchedAt: 'TODO',
38 | }
39 |
40 | // We don't need the app to be mounted to know that we want to fetch the pokemon
41 | // named "pikachu" so we can go ahead and do that right here.
42 | // 🐨 assign the pokemonPromise variable to a call to fetchPokemon('pikachu')
43 |
44 | // 🐨 when the promise resolves, set the pokemon variable to the resolved value
45 | // 🐨 if the promise fails, set the pokemonError variable to the error
46 |
47 | function PokemonInfo() {
48 | // 🐨 if pokemonError is defined, then throw it here
49 | // 🐨 if there's no pokemon yet, then throw the pokemonPromise
50 | // 💰 (no, for real. Like: `throw pokemonPromise`)
51 |
52 | // if the code gets it this far, then the pokemon variable is defined and
53 | // rendering can continue!
54 | return (
55 |
67 | {/*
68 | 🐨 Wrap the PokemonInfo component with a React.Suspense component with a fallback
69 | 🐨 Then wrap all that with an to catch errors
70 | 💰 I wrote the ErrorBoundary for you. You can take a look at it in the utils file if you want
71 | */}
72 |
73 |
74 | )
75 | }
76 |
77 | /*
78 | 🦉 Elaboration & Feedback
79 | After the instruction, copy the URL below into your browser and fill out the form:
80 | http://ws.kcd.im/?ws=Concurrent%20React&e=Simple%20Data-fetching&em=
81 | */
82 |
83 | ////////////////////////////////////////////////////////////////////
84 | // //
85 | // Don't make changes below here. //
86 | // But do look at it to see how your code is intended to be used. //
87 | // //
88 | ////////////////////////////////////////////////////////////////////
89 |
90 | export default App
91 |
--------------------------------------------------------------------------------
/src/exercises/01.md:
--------------------------------------------------------------------------------
1 | # Simple Data-fetching
2 |
3 | > 🦉 Make sure Kent shows you how to enable Concurrent Mode. It's pretty simple,
4 | > so there's no exercise, but don't let him forget to show you! Oh, and he wrote
5 | > a blog post about it too:
6 | > https://kentcdodds.com/blog/how-to-enable-react-concurrent-mode
7 |
8 | ## Background
9 |
10 | We're going to start off as simple as possible, but it's still going to feel a
11 | little different from anything you've probably done with async data loading
12 | before, so buckle up.
13 |
14 | First, there's the Suspense API. Here's the basic idea:
15 |
16 | ```javascript
17 | function Component() {
18 | if (data) {
19 | return
{data.message}
20 | }
21 | throw promise
22 | // React with catch this, find the closest "Suspense" component
23 | // and "suspend" everything from there down from rendering until the
24 | // promise resolves.
25 | // 🚨 THIS "API" IS LIKELY TO CHANGE
26 | }
27 |
28 | ReactDOM.createRoot(rootEl).render(
29 | loading...}>
30 |
31 | ,
32 | )
33 | ```
34 |
35 | That's the idea. Where the `data` and `promise` values are coming from all
36 | depends on how you implement things, and that's what we're going to do in this
37 | exercise.
38 |
39 | Imagine when your app loads, you need some data before you can show anything
40 | useful. Typically we want to put the data loading requirements right in the
41 | component that requires the data, via something like this:
42 |
43 | ```javascript
44 | React.useEffect(() => {
45 | let current = true
46 | setState({status: 'pending'})
47 | doAsyncThing().then(
48 | p => {
49 | if (current) setState({pokemon: p, status: 'success'})
50 | },
51 | e => {
52 | if (current) setState({error: e, status: 'error'})
53 | },
54 | )
55 | return () => (current = false)
56 | }, [pokemonName])
57 |
58 | // render stuff based on the state
59 | ```
60 |
61 | However, for "bootstrap" type data, we can start that request before we even
62 | render the app. The best approaches to using Suspense involve kicking off the
63 | request for the data as soon as you have the information you need for the
64 | request. This is called the "Render as you fetch" approach. We'll get into that
65 | a little bit more in future exercises, but keep that in mind.
66 |
67 | 📜 Here are some docs for you if you need them:
68 |
69 | - [``](https://reactjs.org/docs/concurrent-mode-reference.html#suspense)
70 |
71 | ### Promises
72 |
73 | Through all of this you should have a firm understanding of promises. Here's a
74 | quick example though:
75 |
76 | ```javascript
77 | const handleSuccess = result => console.log(result)
78 | const handleFailure = error => console.error(error)
79 |
80 | const myPromise = someAsyncFunction().then(handleSuccess, handleFailure)
81 | ```
82 |
83 | ## Exercise
84 |
85 | In this exercise, we have a page that's specifically for the pokemon named
86 | Pikachu and we want to load Pikachu's data as soon as the app starts. You're
87 | going to use an `ErrorBoundary` that I've built for you (as relevant as they are
88 | to this topic, the concept of `Error Boundaries` long pre-dates Suspense, so we
89 | won't be getting into it for this workshop). You'll also be using the
90 | `React.Suspense` API.
91 |
92 | ## Extra Credit
93 |
94 | ### 💯 add error handling with an Error Boundary
95 |
96 | What happens if you mistype `pikachu` and instead try to request `pikacha`? This
97 | will result in an error and we need to handle this.
98 |
99 | In React, the way we handle component errors is with an `ErrorBoundary`.
100 |
101 | > 📜 Read up on
102 | > [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) if you
103 | > haven't used them much before.
104 |
105 | We've got an `ErrorBoundary` component all built-out for you and you can import
106 | it from the `utils` file (It's where we're getting the `PokemonDataView`
107 | component right now).
108 |
109 | So you'll wrap your component in an ErrorBoundary for handling that error. But
110 | then you need to turn your promise's error into an error the ErrorBoundary can
111 | handle.
112 |
113 | For this extra credit, think of the error as similar to the pokemon data. You'll
114 | need a handler to get access to the error object, and then instead of _using_ it
115 | in your JSX, you can simply _throw_ it in your render method:
116 |
117 | ```javascript
118 | function Example() {
119 | if (error) {
120 | throw error
121 | }
122 | // ... etc
123 | }
124 | ```
125 |
126 | Give that a try!
127 |
128 | ### 💯 make more generic createResource
129 |
130 | This is also a JavaScript refactor, but in this case we want to make a generic
131 | "resource factory" which has the following API:
132 |
133 | ```javascript
134 | const resource = createResource(() => someAsyncThing())
135 |
136 | function MyComponent() {
137 | const myData = resource.read()
138 | // render myData stuff
139 | }
140 | ```
141 |
142 | Try to refactor your code a bit to have a resource factory we can use for all
143 | our async needs.
144 |
145 | ### 💯 Use utils
146 |
147 | There are a bunch of utilities in the `src/utils.js` file that we'll be using a
148 | bunch during this workshop. Refactor your code to use those so you're familiar
149 | with them. Two you can use for this one are: `PokemonInfoFallback` and
150 | `createResource`!
151 |
--------------------------------------------------------------------------------
/src/exercises/02.js:
--------------------------------------------------------------------------------
1 | // Render as you fetch
2 |
3 | // http://localhost:3000/isolated/exercises/02
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | // ErrorBoundary,
9 | // createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | // 🐨 Your goal is to refactor this traditional useEffect-style async
26 | // interaction to suspense with resources. Enjoy!
27 |
28 | function PokemonInfo({pokemonName}) {
29 | // 💣 you're pretty much going to delete all this stuff except for the one
30 | // place where 🐨 appears
31 | const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
32 | pokemon: null,
33 | error: null,
34 | status: 'pending',
35 | })
36 |
37 | const {pokemon, error, status} = state
38 |
39 | React.useEffect(() => {
40 | let current = true
41 | setState({status: 'pending'})
42 | fetchPokemon(pokemonName).then(
43 | p => {
44 | if (current) setState({pokemon: p, status: 'success'})
45 | },
46 | e => {
47 | if (current) setState({error: e, status: 'error'})
48 | },
49 | )
50 | return () => (current = false)
51 | }, [pokemonName])
52 |
53 | // 💰 This will be the fallback prop of
54 | if (status === 'pending') {
55 | return
56 | }
57 |
58 | // 💰 This is the same thing the ErrorBoundary renders
59 | if (status === 'error') {
60 | return (
61 |
62 | There was an error.
63 |
{error.message}
64 |
65 | )
66 | }
67 |
68 | // 💰 this is the part that will suspend
69 | if (status === 'success') {
70 | // 🐨 instead of accpeting the pokemonName as a prop to this component
71 | // you'll accept a pokemonResource.
72 | // 💰 you'll get the pokemon from: pokemonResource.read()
73 | // 🐨 This will be the return value of this component. You wont need it
74 | // to be in this if statement anymore thought!
75 | return (
76 |
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 | function App() {
87 | const [pokemonName, setPokemonName] = React.useState(null)
88 | // 🐨 add a useState here to keep track of the current pokemonResource
89 |
90 | function handleSubmit(newPokemonName) {
91 | setPokemonName(newPokemonName)
92 | // 🐨 set the pokemon resource right here
93 | }
94 |
95 | return (
96 |
97 |
98 |
99 |
100 | {pokemonName ? ( // 🐨 instead of pokemonName, use pokemonResource here
101 | // 🐨 wrap PokemonInfo in an ErrorBoundary and React.Suspense component
102 | // to manage the error and loading states that PokemonInfo was managing
103 | // before your changes.
104 |
105 | ) : (
106 | 'Submit a pokemon'
107 | )}
108 |
109 |
110 | )
111 | }
112 |
113 | /*
114 | 🦉 Elaboration & Feedback
115 | After the instruction, copy the URL below into your browser and fill out the form:
116 | http://ws.kcd.im/?ws=Concurrent%20React&e=Refactor%20from%20useEffect&em=
117 | */
118 |
119 | ////////////////////////////////////////////////////////////////////
120 | // //
121 | // Don't make changes below here. //
122 | // But do look at it to see how your code is intended to be used. //
123 | // //
124 | ////////////////////////////////////////////////////////////////////
125 |
126 | export default App
127 |
--------------------------------------------------------------------------------
/src/exercises/02.md:
--------------------------------------------------------------------------------
1 | # Render as you fetch
2 |
3 | ## Background
4 |
5 | This one's a bit of a mind bender, but here's the ultimate goal we're looking
6 | for: https://twitter.com/kentcdodds/status/1191922859762843649
7 |
8 | We wont get the whole way there in this exercise, but we'll get a bunch of the
9 | way there. Then in exercise 04 we can finish it up. So fast ⚡
10 |
11 | The idea here is that: get the data as soon as you have the information you need
12 | for the data. This sounds obvious, but if you think about it, how often do you
13 | have a component that requests data once it's been mounted. There's a few
14 | milliseconds between the time you click "go" and the time that component is
15 | mounted... Unless that component's code is lazy-loaded. In which case, there's a
16 | lot more time involved and your users are hanging around waiting while they
17 | could be making requests for the data they need.
18 |
19 | That's the entire idea behind "Render as you fetch."
20 |
21 | The information often involves a user's
22 |
23 | ## Exercise
24 |
25 | In this one, we now have a form that allows us to choose a pokemon by any name.
26 | As soon as the user hits "submit", we pass the `pokemonName` to our
27 | `PokemonInfo` component which makes the request to get the pokemon data (using
28 | `useEffect`).
29 |
30 | For the exercise, you need to refactor this from `useEffect` to Suspense. You'll
31 | need to add the `ErrorBoundary` and `Suspense` components to the `PokemonInfo`
32 | component, and you'll pass the pokemon resource to `PokemonInfoView` which will
33 | call `.read()` on the resource. The initial `.read()` call will trigger the
34 | component to suspend and display the fallback state. When the promise resolves,
35 | React will re-render our components and we'll be able to display the pokemon.
36 |
37 | > The real important parts of the render-as-you-fetch approach comes in the
38 | > extra credit, but changing things to this will help a lot to get us going.
39 |
40 | ## Extra Credit
41 |
42 | So far, we've benefitted from an API standpoint. I think the Suspense solution
43 | is simpler than the `useEffect` version. However, we've not gotten the "Render
44 | as you fetch" benefit when it comes to asynchronously loading the code we need.
45 |
46 | These extra credit allow you to compare the two approaches.
47 |
48 | 🦉 For both of these, we're calling `window.fetch.restoreOriginalFetch()` at the
49 | top of our file so our fetch requests actually hit the network so we can see
50 | them in the network tab.
51 |
52 | ### 💯 Suspense and Error Boundary positioning
53 |
54 | You don't have to wrap the suspending component in a suspense and error boundary
55 | directly. There can be many layers of nesting and it'll still work. But there's
56 | some semantically important differences that I want you to learn about so go
57 | ahead and try to play around with wrapping more of your elements in these
58 | boundaries and see what changes with the user experience.
59 |
--------------------------------------------------------------------------------
/src/exercises/03.js:
--------------------------------------------------------------------------------
1 | // useTransition for improved loading states
2 |
3 | // http://localhost:3000/isolated/exercises/03
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | function PokemonInfo({pokemonResource}) {
26 | const pokemon = pokemonResource.read()
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | // try a few of these fetch times:
38 | // shows busy indicator
39 | // window.FETCH_TIME = 450
40 |
41 | // shows busy indicator, then suspense fallback
42 | // window.FETCH_TIME = 5000
43 |
44 | // shows busy indicator for a split second
45 | // 💯 this is what the extra credit improves
46 | // window.FETCH_TIME = 200
47 |
48 | // 🐨 create a SUSPENSE_CONFIG variable right here and configure timeoutMs to
49 | // whatever feels right to you, then try it out and tweek it until you're happy
50 | // with the experience.
51 |
52 | function createPokemonResource(pokemonName) {
53 | return createResource(() => fetchPokemon(pokemonName))
54 | }
55 |
56 | function App() {
57 | const [pokemonName, setPokemonName] = React.useState(null)
58 | // 🐨 add a useTransition hook here
59 | const [pokemonResource, setPokemonResource] = React.useState(null)
60 |
61 | function handleSubmit(newPokemonName) {
62 | setPokemonName(newPokemonName)
63 | // 🐨 wrap this next line in a startTransition call
64 | setPokemonResource(createPokemonResource(newPokemonName))
65 | // 🦉 what do you think would happen if you put the setPokemonName above
66 | // into the `startTransition` call? Go ahead and give that a try!
67 | }
68 |
69 | return (
70 |
71 |
72 |
73 | {/*
74 | 🐨 add inline styles here to set the opacity to 0.6 if the
75 | useTransition above is pending
76 | */}
77 |
91 | )
92 | }
93 |
94 | /*
95 | 🦉 Elaboration & Feedback
96 | After the instruction, copy the URL below into your browser and fill out the form:
97 | http://ws.kcd.im/?ws=Concurrent%20React&e=useTransition%20for%20improved%20loading%20states&em=
98 | */
99 |
100 | ////////////////////////////////////////////////////////////////////
101 | // //
102 | // Don't make changes below here. //
103 | // But do look at it to see how your code is intended to be used. //
104 | // //
105 | ////////////////////////////////////////////////////////////////////
106 |
107 | export default App
108 |
--------------------------------------------------------------------------------
/src/exercises/03.md:
--------------------------------------------------------------------------------
1 | # useTransition for improved loading states
2 |
3 | ## Background
4 |
5 | When a component suspends, it's literally telling React: "Don't render any
6 | updates at all from the suspense component on down until I'm ready to roll."
7 | Now, eventually React will give up on the suspending component and render your
8 | fallback instead. But there's that brief amount of time that your app will
9 | appear to be unresponsive to the user and it'd be great if we could avoid that.
10 |
11 | Also, you're probably seeing an error in your console right about now and it'd
12 | be cool to make that go away 😉
13 |
14 | The API for this is a hook called `useTransition`. Here's what that looks like:
15 |
16 | ```javascript
17 | const SUSPENSE_CONFIG = {timeoutMs: 4000}
18 |
19 | function Component() {
20 | const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
21 | // etc...
22 |
23 | function handleClick() {
24 | // do something that triggers some interum state change we want to
25 | // happen before suspending starts
26 | startTransition(() => {
27 | // do something that triggers a suspending component to render
28 | })
29 | }
30 |
31 | // if needed, you can use the `isPending` boolean to display a loading spinner
32 | // or similar
33 | }
34 | ```
35 |
36 | ## Exercise
37 |
38 | In this exercise, we'll wrap the existing call to set the resource in a
39 | transition so the input value gets updated when you select a pokemon. We'll also
40 | make the pokemon information area "appear stale" by making it slightly
41 | transparent if `isPending` is true.
42 |
43 | This is a good one to play around with setting `window.FETCH_TIME` a bit.
44 |
45 | ## Extra Credit
46 |
47 | ### 💯 use css transitions
48 |
49 | If the user has a really fast connection, then they'll see a "flash of loading
50 | content" which isn't a great experience. To combat this, I've written a css rule
51 | that has a transition delay for the opacity to not become transparent for 300
52 | milliseconds. That way if the user's on a fast connection, they wont see the
53 | loading state.
54 |
55 | Instead of using inline styles, dynamically apply the class name
56 | `pokemon-loading` if `isPending` is true to take advantage of this. The styles
57 | are in `src/styles.css` if you want to take a look.
58 |
59 | ### 💯 avoid flash of loading content
60 |
61 | **EXPERIMENTAL AND AWKWARD API AHEAD**
62 |
63 | Our previous improvement is great. We're not showing the loading state for 300ms
64 | so we're pretty good. But what if the request takes 350ms? Then we're right back
65 | where we started! The user will see a flash of loading state for 50ms.
66 |
67 | What we really need is a way to say: "Hey React, if this transition takes 300ms,
68 | then I want you to keep the transition state around for at least 500ms total no
69 | matter what."
70 |
71 | Now, this API is a little strange, it's not documented (so it's pretty likely to
72 | change). In my testing of it, it was kind of inconsistent, so I think it may be
73 | buggy. But to make this happen, you can add the following properties to your
74 | `SUSPENSE_CONFIG`:
75 |
76 | - `busyDelayMs`: Set this to the time of our CSS transition. This is the part
77 | that says "if the transition takes X amount of time"
78 | - `busyMinDurationMs`: Set this to the total time you want the transition state
79 | to persist if we surpass the `busyDelayMs` time.
80 |
--------------------------------------------------------------------------------
/src/exercises/04.js:
--------------------------------------------------------------------------------
1 | // Cache resources
2 |
3 | // http://localhost:3000/isolated/exercises/04
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | function PokemonInfo({pokemonResource}) {
26 | const pokemon = pokemonResource.read()
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | const SUSPENSE_CONFIG = {
38 | timeoutMs: 4000,
39 | busyDelayMs: 300, // this time is slightly shorter than our css transition delay
40 | busyMinDurationMs: 700,
41 | }
42 |
43 | // 🐨 create a pokemonResourceCache object
44 |
45 | // 🐨 create a getPokemonResource function which accepts a name checks the cache
46 | // for an existing resource. If there is none, then it creates a resource
47 | // and inserts it into the cache. Finally the function should return the
48 | // resource.
49 |
50 | function createPokemonResource(pokemonName) {
51 | return createResource(() => fetchPokemon(pokemonName))
52 | }
53 |
54 | function App() {
55 | const [pokemonName, setPokemonName] = React.useState('')
56 | const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
57 | const [pokemonResource, setPokemonResource] = React.useState(null)
58 |
59 | function handleSubmit(newPokemonName) {
60 | setPokemonName(newPokemonName)
61 | startTransition(() => {
62 | // 🐨 change this to getPokemonResource instead
63 | setPokemonResource(createPokemonResource(newPokemonName))
64 | })
65 | }
66 |
67 | return (
68 |
85 | )
86 | }
87 |
88 | /*
89 | 🦉 Elaboration & Feedback
90 | After the instruction, copy the URL below into your browser and fill out the form:
91 | http://ws.kcd.im/?ws=Concurrent%20React&e=Cache%20resources&em=
92 | */
93 |
94 | ////////////////////////////////////////////////////////////////////
95 | // //
96 | // Don't make changes below here. //
97 | // But do look at it to see how your code is intended to be used. //
98 | // //
99 | ////////////////////////////////////////////////////////////////////
100 |
101 | export default App
102 |
--------------------------------------------------------------------------------
/src/exercises/04.md:
--------------------------------------------------------------------------------
1 | # Cache resources
2 |
3 | ## Background
4 |
5 | State that comes from the server is basically a cache of state. It's not UI
6 | state. How long that cache sticks around is totally up to you. Right now, our
7 | cache only hangs around until we select a new resource, but we could persist it
8 | in memory somewhere and retrieve it later if needed.
9 |
10 | Remember that caches are among the hardest problems in computer science. If
11 | you're not careful, you can run into stale data bugs and memory leaks. But let's
12 | experiment with it here and see if we can improve the user experience.
13 |
14 | ### Promises in render
15 |
16 | 💰 here's a quick tip. Creating a new promise in the render method is dangerous
17 | because you cannot rely on your render method only being called once, so you
18 | have to do things carefully by using a promise cache. Here's an example of what
19 | I mean:
20 |
21 | ```javascript
22 | const promiseCache = {}
23 | function MySuspendingComponent({value}) {
24 | let resource = promiseCache[value]
25 | if (!resource) {
26 | resource = doAsyncThing(value)
27 | promiseCache[value] = resource // <-- this is very important
28 | }
29 | return
{resource.read()}
30 | }
31 | ```
32 |
33 | You'll be doing something similar for this exercise.
34 |
35 | ## Exercise
36 |
37 | If you select a pokemon, then choose another, and then go back to the pokemon
38 | you selected the first time, you'll notice that you're loading that first
39 | pokemon twice, even though the data hasn't changed. That data is unlikely to
40 | ever change, so we could improve the user experience considerably by caching the
41 | data so it's available for the next time the user wants to look at that pokemon.
42 |
43 | In this exercise, our cache will function very similar to the image cache we had
44 | in the last exercise. The cache key will be the pokemon name, and the cache
45 | value will be the resource object.
46 |
--------------------------------------------------------------------------------
/src/exercises/05.js:
--------------------------------------------------------------------------------
1 | // Suspense Image
2 |
3 | // http://localhost:3000/isolated/exercises/05
4 |
5 | import React from 'react'
6 | import fetchPokemon from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | PokemonInfoFallback,
11 | PokemonForm,
12 | PokemonDataView,
13 | } from '../utils'
14 |
15 | // By default, all fetches are mocked so we can control the time easily.
16 | // You can adjust the fetch time with this:
17 | // window.FETCH_TIME = 3000
18 | // If you want to make an actual network call for the pokemon
19 | // then uncomment the following line
20 | // window.fetch.restoreOriginalFetch()
21 | // Note that by doing this, the FETCH_TIME will no longer be considered
22 | // and if you want to slow things down you should use the Network tab
23 | // in your developer tools to throttle your network to something like "Slow 3G"
24 |
25 | // 🦉 On this one, make sure that you uncheck the "Disable cache" checkbox.
26 | // We're relying on that cache for this approach to work!
27 |
28 | // we need to make a place to store the resources outside of render so
29 | // 🐨 create "cache" object here.
30 |
31 | // 🐨 create an Img component that renders a regular and accepts a src
32 | // prop and forwards on any remaining props.
33 | // 🐨 The first thing you do in this component is check wither your
34 | // imgSrcResourceCache already has a resource for the given src prop. If it does
35 | // not, then you need to create one (💰 using createResource).
36 | // 🐨 Once you have the resource, then render the .
37 | // 💰 Here's what rendering the should look like:
38 | //
39 |
40 | function PokemonInfo({pokemonResource}) {
41 | const pokemon = pokemonResource.read()
42 | return (
43 |
44 |
45 | {/* 🐨 swap this img for your new Img component */}
46 |
47 |
105 | )
106 | }
107 |
108 | /*
109 | 🦉 Elaboration & Feedback
110 | After the instruction, copy the URL below into your browser and fill out the form:
111 | http://ws.kcd.im/?ws=Concurrent%20React&e=Suspense%20Image&em=
112 | */
113 |
114 | ////////////////////////////////////////////////////////////////////
115 | // //
116 | // Don't make changes below here. //
117 | // But do look at it to see how your code is intended to be used. //
118 | // //
119 | ////////////////////////////////////////////////////////////////////
120 |
121 | export default App
122 |
--------------------------------------------------------------------------------
/src/exercises/05.md:
--------------------------------------------------------------------------------
1 | # Suspense Image
2 |
3 | ## Background
4 |
5 | Loading images is tricky business because you're handing the asynchronous state
6 | over to the browser. It manages the loading, error, and success states for you.
7 | But what if you have an experience that doesn't look any good until the image is
8 | actually loaded? Or what if you want to render a fallback in the image's place
9 | while it's loading (you want to provide your own loading UI)? In that case,
10 | you're kinda out of luck, because the browser gives us no such API.
11 |
12 | Suspense can help us with this too! Luckily for us, we can pre-load images into
13 | the browser's cache using the following code:
14 |
15 | ```javascript
16 | function preloadImage(src) {
17 | return new Promise(resolve => {
18 | const img = document.createElement('img')
19 | img.src = src
20 | img.onload = () => resolve(src)
21 | })
22 | }
23 | ```
24 |
25 | That function will resolve to the source you gave it as soon as the image has
26 | loaded. Once that promise resolves, you know that the browser has it in it's
27 | cache and any `` elements you render with the `src` set to that `src`
28 | value will get instantly rendered with the image straight from the browser
29 | cache.
30 |
31 | ## Exercise
32 |
33 | If you turn up the throttle on your network tab (to "Slow 3G" for example) and
34 | select pokemon, you may notice that images take a moment to load in.
35 |
36 | For the first one, there's nothing there and then it bumps the content down when
37 | it loads. This can be "fixed" by setting a fixed height for the images. But
38 | let's assume that you can't be sure what that height is.
39 |
40 | If you select another pokemon, then that pokemon's data pops in, but the old
41 | pokemon's image remains in place until the new one's image finishes loading.
42 |
43 | With suspense, we have an opportunity to make this experience a lot better. We
44 | have two related options:
45 |
46 | 1. Make an `Img` component that suspends until the browser has actually loaded
47 | the image.
48 | 2. Make a request for the image alongside the pokemon data.
49 |
50 | Option 1 means that nothing will render until both the data and the image are
51 | ready.
52 |
53 | Option 2 is even better because it loads the data and image at the same time. It
54 | works because all the images are available via the same information we use to
55 | get the pokemon data.
56 |
57 | We're going to do both of these approaches for this exercise (option 2 is extra
58 | credit).
59 |
60 | ## Extra Credit
61 |
62 | ### 💯 avoid waterfall
63 |
64 | If you open up the network tab, you'll notice that you have to load the data
65 | before you can load the image because the data is where we get the image URL.
66 | You may also notice that the image URL is always very predictable. In fact, I
67 | even wrote a function for you to get the image URL based on the pokemon name!
68 | It's exported by `src/fetch-pokemon.js` and is called `getImageUrlForPokemon`.
69 |
70 | ```javascript
71 | const imageUrl = getImageUrlForPokemon('pikachu')
72 | ```
73 |
74 | Try to pre-load this at the same time as the rest of your data. This one will be
75 | a bit trickier. I'll give you a hint. There are several ways you could do this,
76 | but in my solution, I end up changing the `PokemonInfo` component to this:
77 |
78 | ```javascript
79 | function PokemonInfo({pokemonResource}) {
80 | const pokemon = pokemonResource.data.read()
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
90 | ```
91 |
92 | ### 💯 Render as you Fetch
93 |
94 | Remember when we did this with 02? Now that we're pre-loading the image along
95 | with the data, the improvements will be even more pronounced. Go ahead and put
96 | this at the top of your file:
97 |
98 | ```javascript
99 | import createPokemonInfoResource from '../lazy/pokemon-info-render-as-you-fetch-04.data'
100 |
101 | const PokemonInfo = React.lazy(() =>
102 | import('../lazy/pokemon-info-render-as-you-fetch-04'),
103 | )
104 | ```
105 |
106 | And make that work. Then checkout the network tab and see how your waterfall has
107 | turned into a... stone wall? Yeah!
108 |
109 | Again this is a good one to have `window.fetch.restoreOriginalFetch()` called so
110 | you can see a real network request for the data. You will notice that we're
111 | making two requests to the pokemon endpoint. The first is an OPTIONS request.
112 | That's basically a request the browser makes to ask the server if it will accept
113 | our request across domains. It's kind of annoying because it makes our network
114 | requests take longer, but it is what it is.
115 |
--------------------------------------------------------------------------------
/src/exercises/06.js:
--------------------------------------------------------------------------------
1 | // Suspense with a custom hook
2 |
3 | // http://localhost:3000/isolated/exercises/06
4 |
5 | import React from 'react'
6 | import fetchPokemon, {getImageUrlForPokemon} from '../fetch-pokemon'
7 | import {
8 | ErrorBoundary,
9 | createResource,
10 | preloadImage,
11 | PokemonInfoFallback,
12 | PokemonForm,
13 | PokemonDataView,
14 | } from '../utils'
15 |
16 | // By default, all fetches are mocked so we can control the time easily.
17 | // You can adjust the fetch time with this:
18 | // window.FETCH_TIME = 3000
19 | // If you want to make an actual network call for the pokemon
20 | // then uncomment the following line
21 | // window.fetch.restoreOriginalFetch()
22 | // Note that by doing this, the FETCH_TIME will no longer be considered
23 | // and if you want to slow things down you should use the Network tab
24 | // in your developer tools to throttle your network to something like "Slow 3G"
25 |
26 | function PokemonInfo({pokemonResource}) {
27 | const pokemon = pokemonResource.data.read()
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const SUSPENSE_CONFIG = {
39 | timeoutMs: 4000,
40 | busyDelayMs: 300, // this time is slightly shorter than our css transition delay
41 | busyMinDurationMs: 700,
42 | }
43 |
44 | const pokemonResourceCache = {}
45 |
46 | function getPokemonResource(name) {
47 | const lowerName = name.toLowerCase()
48 | let resource = pokemonResourceCache[lowerName]
49 | if (!resource) {
50 | resource = createPokemonResource(lowerName)
51 | pokemonResourceCache[lowerName] = resource
52 | }
53 | return resource
54 | }
55 |
56 | function createPokemonResource(pokemonName) {
57 | const lowerName = pokemonName
58 | const data = createResource(() => fetchPokemon(lowerName))
59 | const image = createResource(() =>
60 | preloadImage(getImageUrlForPokemon(lowerName)),
61 | )
62 | return {data, image}
63 | }
64 |
65 | function App() {
66 | const [pokemonName, setPokemonName] = React.useState('')
67 | // 🐨 move these two lines to a custom hook called usePokemonResource
68 |
69 | // 🐨 call usePokemonResource with the pokemonName.
70 | // It should return both the pokemonResource and isPending
71 | const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
72 | const [pokemonResource, setPokemonResource] = React.useState(null)
73 |
74 | function handleSubmit(newPokemonName) {
75 | setPokemonName(newPokemonName)
76 | // 🐨 move this startTransition call to a useLayoutEffect inside your
77 | // custom usePokemonResource hook (it should list pokemonName as a
78 | // dependency).
79 | startTransition(() => {
80 | setPokemonResource(getPokemonResource(newPokemonName))
81 | })
82 | // 💰 tip: in your effect callback, if pokemonName is an empty string,
83 | // return early.
84 | }
85 |
86 | return (
87 |
104 | )
105 | }
106 |
107 | /*
108 | 🦉 Elaboration & Feedback
109 | After the instruction, copy the URL below into your browser and fill out the form:
110 | http://ws.kcd.im/?ws=Concurrent%20React&e=Suspense%20with%20a%20custom%20hook&em=
111 | */
112 |
113 | ////////////////////////////////////////////////////////////////////
114 | // //
115 | // Don't make changes below here. //
116 | // But do look at it to see how your code is intended to be used. //
117 | // //
118 | ////////////////////////////////////////////////////////////////////
119 |
120 | export default App
121 |
--------------------------------------------------------------------------------
/src/exercises/06.md:
--------------------------------------------------------------------------------
1 | # Suspense with a custom hook
2 |
3 | ## Background
4 |
5 | React Hooks are amazing. Combine them with React Suspense, and you get some
6 | really awesome APIs.
7 |
8 | ## Exercise
9 |
10 | In this exercise, you're going to create a `usePokemonResource` with the
11 | following API:
12 |
13 | ```javascript
14 | const [pokemonResource, isPending] = usePokemonResource(pokemonName)
15 | ```
16 |
17 | This way users of your hook don't need to bother calling `startTransition` or
18 | anything. Your custom hook will take care of that. Any time the `pokemonName`
19 | changes, your hook will trigger an update to the pokemonResource.
20 |
21 | Note: Currently there are two bugs you'll want to be aware of:
22 |
23 | - https://github.com/facebook/react/issues/17272: Have to `useLayoutEffect` and
24 | cannot `useEffect` currently
25 | - https://github.com/facebook/react/issues/17273: Cannot include
26 | `startTransition` in the effect dependency array
27 |
--------------------------------------------------------------------------------
/src/exercises/07.js:
--------------------------------------------------------------------------------
1 | // Coordinate Suspending components with SuspenseList
2 |
3 | // http://localhost:3000/isolated/exercises/07
4 |
5 | import React from 'react'
6 | import '../suspense-list/style-overrides.css'
7 | import * as cn from '../suspense-list/app.module.css'
8 | import Spinner from '../suspense-list/spinner'
9 | import {createResource, ErrorBoundary, PokemonForm} from '../utils'
10 | import {fetchUser} from '../fetch-pokemon'
11 |
12 | // 💰 this delay function just allows us to make a promise take longer to resolve
13 | // so we can easily play around with the loading time of our code.
14 | const delay = time => promiseResult =>
15 | new Promise(resolve => setTimeout(() => resolve(promiseResult), time))
16 |
17 | // 🐨 feel free to play around with the delay timings.
18 | const NavBar = React.lazy(() =>
19 | import('../suspense-list/nav-bar').then(delay(500)),
20 | )
21 | const LeftNav = React.lazy(() =>
22 | import('../suspense-list/left-nav').then(delay(2000)),
23 | )
24 | const MainContent = React.lazy(() =>
25 | import('../suspense-list/main-content').then(delay(1500)),
26 | )
27 | const RightNav = React.lazy(() =>
28 | import('../suspense-list/right-nav').then(delay(1000)),
29 | )
30 |
31 | const fallback = (
32 |
53 | )
54 | }
55 |
56 | // 🐨 Use React.SuspenseList throughout these Suspending components to make
57 | // them load in a way that is not jaring to the user.
58 | // 💰 there's not really a specifically "right" answer for this.
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | /*
82 | 🦉 Elaboration & Feedback
83 | After the instruction, copy the URL below into your browser and fill out the form:
84 | http://ws.kcd.im/?ws=Concurrent%20React&e=Coordinate%20Suspending%20components%20with%20SuspenseList&em=
85 | */
86 |
87 | ////////////////////////////////////////////////////////////////////
88 | // //
89 | // Don't make changes below here. //
90 | // But do look at it to see how your code is intended to be used. //
91 | // //
92 | ////////////////////////////////////////////////////////////////////
93 |
94 | export default App
95 |
--------------------------------------------------------------------------------
/src/exercises/07.md:
--------------------------------------------------------------------------------
1 | # Coordinate Suspending components with SuspenseList
2 |
3 | ## Background
4 |
5 | When your app is simple, you can pretty much expect everything to be there and
6 | load together when you need them, and that works nicely. But when your app grows
7 | and you start code splitting and loading data alongside the code that needs it,
8 | pretty soon you end up in situations where you have several things loading all
9 | at once. Having those all pop into place on the page can be a jarring experience
10 | for the user.
11 |
12 | A better experience for the user is a more predictable loading experience, even
13 | if it means that they see the data displayed out of order from how it was
14 | loaded.
15 |
16 | Coordinating these loading states is a really hard problem, but thanks to
17 | Suspense and ``, it's fairly trivial.
18 |
19 | 📜 Example from the React docs:
20 | https://reactjs.org/docs/concurrent-mode-reference.html#suspenselist
21 |
22 | ```jsx
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ```
35 |
36 | The `SuspenseList` component has the following props:
37 |
38 | - `revealOrder`: the order in which the suspending components are to render
39 | - `{undefined}`: the default behavior: everything pops in when it's loaded (as
40 | if you didn't wrap everything in a `SuspenseList`).
41 | - `"forwards"`: Only show the component when all components before it have
42 | finished suspending.
43 | - `"backwards"`: Only show the component when all the components after it have
44 | finished suspending.
45 | - `"together"`: Don't show any of the components until they've all finished
46 | loading
47 | - `tail`: determines how to show the fallbacks for the suspending components
48 | - `{undefined}`: the default behavior: show all fallbacks
49 | - `"collapsed"`: Only show the fallback for the component that should be
50 | rendered next (this will differ based on the `revealOrder` specified).
51 | - `"hidden"`: Opposite of the default behavior: show none of the fallbacks
52 | - `children`: other react elements which render `` components.
53 | Note: `` components do not have to be direct children as in
54 | the example above. You can wrap them in ``s or other components if you
55 | need.
56 |
57 | ## Exercise
58 |
59 | In this exercise, we've built Pokemon Banking app and because the app is getting
60 | so large and there's so many dynamic parts, we've decided to codesplit a lot of
61 | it, this makes our app load faster, but it makes the loading experience
62 | sub-optimal.
63 |
64 | Let's play around with `` to coordiante the loading
65 | states.
66 |
67 | 💰 tip, you can nest Suspense lists! Give that a try.
68 |
69 | ## Extra Credit
70 |
71 | ### 💯 eagerly load modules as resources
72 |
73 | You may have noticed that the page actually loads slower when you added the
74 | SuspenseList. Even though it's less janky. The reason for this is because
75 | SuspenseList will avoid rendering children of Suspense Boundaries when using
76 | `forwards` until the "most forward" component is rendered. This results in a
77 | waterfall effect because `React.lazy` is not "eager." The idiomatic use of
78 | `React.lazy` is a perfect example of "fetch on render."
79 |
80 | Hopefully the React team comes out with something better before SuspenseList is
81 | stable (stay up-to-date with https://github.com/facebook/react/issues/17413),
82 | but for now, we need to preload our modules as soon as we know we're going to
83 | need them.
84 |
85 | I've written a handy function that you can use in place of `React.lazy` that
86 | allows you to do this:
87 |
88 | ```javascript
89 | function preloadableLazy(dynamicImport) {
90 | let promise
91 | function load() {
92 | if (!promise) {
93 | promise = dynamicImport()
94 | }
95 | return promise
96 | }
97 | const Comp = React.lazy(load)
98 | Comp.preload = load
99 | return Comp
100 | }
101 | // Usage:
102 | // const LazyComp = preloadableLazy(() => import('./lazy-loaded-component'))
103 | // then, when you need to preload the code: LazyComp.preload()
104 | ```
105 |
106 | With this, see if you can change the implementation to make use of the
107 | SuspenseList for coordinating suspending components without the drawback of
108 | "fetch on render" with `React.lazy`.
109 |
--------------------------------------------------------------------------------
/src/fetch-pokemon.js:
--------------------------------------------------------------------------------
1 | import transactions from './hacks/transactions'
2 | import users from './hacks/users'
3 | import pkg from '../package.json'
4 | // if you need this to work locally then comment out the import above and comment in the next line
5 | // const pkg = {homepage: '/'}
6 |
7 | const sleep = time => new Promise(resolve => setTimeout(resolve, time))
8 |
9 | const formatDate = date =>
10 | `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')} ${String(
11 | date.getSeconds(),
12 | ).padStart(2, '0')}.${String(date.getMilliseconds()).padStart(3, '0')}`
13 |
14 | // the delay argument is for faking things out a bit
15 | function fetchPokemon(name, delay = 1500) {
16 | const endTime = Date.now() + delay
17 | const pokemonQuery = `
18 | query ($name: String) {
19 | pokemon(name: $name) {
20 | id
21 | number
22 | name
23 | image
24 | attacks {
25 | special {
26 | name
27 | type
28 | damage
29 | }
30 | }
31 | }
32 | }
33 | `
34 |
35 | return window
36 | .fetch('https://graphql-pokemon.now.sh', {
37 | // learn more about this API here: https://graphql-pokemon.now.sh/
38 | method: 'POST',
39 | headers: {
40 | 'content-type': 'application/json;charset=UTF-8',
41 | },
42 | body: JSON.stringify({
43 | query: pokemonQuery,
44 | variables: {name: name.toLowerCase()},
45 | }),
46 | })
47 | .then(response => response.json())
48 | .then(async response => {
49 | await sleep(endTime - Date.now())
50 | return response
51 | })
52 | .then(response => {
53 | const pokemon = response.data.pokemon
54 | if (pokemon) {
55 | pokemon.fetchedAt = formatDate(new Date())
56 | return pokemon
57 | } else {
58 | return Promise.reject(new Error(`No pokemon with the name "${name}"`))
59 | }
60 | })
61 | }
62 |
63 | function getImageUrlForPokemon(pokemonName) {
64 | if (fetch.isHacked) {
65 | return `${pkg.homepage}img/pokemon/${pokemonName.toLowerCase()}.jpg`
66 | } else {
67 | return `https://img.pokemondb.net/artwork/${pokemonName.toLowerCase()}.jpg`
68 | }
69 | }
70 |
71 | async function fetchUser(pokemonName, delay = 0) {
72 | await sleep(delay)
73 | const lowerName = pokemonName.toLowerCase()
74 | const pokemonTransactions = transactions.filter(
75 | t => t.recipient !== lowerName,
76 | )
77 | const user = users[lowerName]
78 | if (!user) {
79 | throw new Error(
80 | `${pokemonName} is not a user. Try ${Object.keys(users).join(', ')}`,
81 | )
82 | }
83 | return {
84 | transactions: pokemonTransactions,
85 | friends: Object.keys(users)
86 | .filter(u => lowerName !== u)
87 | .map(n => upperName(n)),
88 | ...user,
89 | name: upperName(lowerName),
90 | }
91 | }
92 | const upperName = name => `${name.slice(0, 1).toUpperCase()}${name.slice(1)}`
93 |
94 | export default fetchPokemon
95 | export {getImageUrlForPokemon, fetchUser}
96 |
--------------------------------------------------------------------------------
/src/hacks/fetch.js:
--------------------------------------------------------------------------------
1 | import allPokemon from './pokemon.json'
2 | // Please don't actually do this in a real app
3 | // this is here to make it easy for us to simulate making HTTP calls in this
4 | // little app that doesn't actually have any server element.
5 | const originalFetch = window.fetch
6 |
7 | // Allows us to restore the original fetch
8 | originalFetch.restoreOriginalFetch = () => (window.fetch = originalFetch)
9 | originalFetch.overrideFetch = () => (window.fetch = hackFetch)
10 |
11 | window.FETCH_TIME = 0
12 | window.MIN_FETCH_TIME = 500
13 | window.FETCH_TIME_RANDOM = false
14 |
15 | function sleep(t = window.FETCH_TIME) {
16 | if (window.FETCH_TIME_RANDOM) {
17 | t = Math.random() * t + window.MIN_FETCH_TIME
18 | }
19 | return new Promise(resolve => setTimeout(resolve, t))
20 | }
21 |
22 | const fakeResponses = [
23 | {
24 | test: url => url.includes('pokemon'),
25 | handler: async (url, config) => {
26 | const body = JSON.parse(config.body)
27 | await sleep()
28 | const pokemonName = body.variables.name
29 | const pokemon = allPokemon[pokemonName]
30 | if (!pokemon) {
31 | throw new Error(
32 | `🚨 fetch calls are "hacked" so you can work this workshop offline, so we don't support the pokemon with the name "${pokemonName}." We only support: ${Object.keys(
33 | allPokemon,
34 | ).join(', ')}`,
35 | )
36 | }
37 | return {
38 | status: 200,
39 | json: async () => ({data: {pokemon}}),
40 | }
41 | },
42 | },
43 | // fallback to originalFetch
44 | {
45 | test: () => true,
46 | handler: (...args) => originalFetch(...args),
47 | },
48 | ]
49 |
50 | async function hackFetch(...args) {
51 | const {handler} = fakeResponses.find(({test}) => {
52 | try {
53 | return test(...args)
54 | } catch (error) {
55 | // ignore the error and hope everything's ok...
56 | return false
57 | }
58 | })
59 | const groupTitle = `%c ${args[1].method} -> ${args[0]}`
60 | try {
61 | const response = await handler(...args)
62 | console.groupCollapsed(groupTitle, 'color: #0f9d58')
63 | let parsedBody
64 | try {
65 | parsedBody = JSON.parse(args[1].body)
66 | } catch (error) {
67 | // ignore
68 | }
69 | console.info('REQUEST:', {
70 | url: args[0],
71 | ...args[1],
72 | ...(parsedBody ? {parsedBody} : null),
73 | })
74 | console.info('RESPONSE:', {
75 | ...response,
76 | ...(response.json ? {json: await response.json()} : {}),
77 | })
78 | console.groupEnd()
79 | return response
80 | } catch (error) {
81 | let rejection = error
82 | if (error instanceof Error) {
83 | rejection = {
84 | status: 500,
85 | message: error.message,
86 | }
87 | }
88 | console.groupCollapsed(groupTitle, 'color: #ef5350')
89 | console.info('REQUEST:', {url: args[0], ...args[1]})
90 | console.info('REJECTION:', rejection)
91 | console.groupEnd()
92 | return Promise.reject(rejection)
93 | }
94 | }
95 | hackFetch.isHacked = true
96 | Object.assign(hackFetch, window.fetch)
97 |
98 | // alright. Let's hack fetch!
99 | window.fetch.overrideFetch()
100 |
--------------------------------------------------------------------------------
/src/hacks/index.js:
--------------------------------------------------------------------------------
1 | import 'stop-runaway-react-effects/hijack'
2 | import './fetch'
3 |
--------------------------------------------------------------------------------
/src/hacks/pokemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "pikachu": {
3 | "id": "UG9rZW1vbjowMjU=",
4 | "number": "025",
5 | "name": "Pikachu",
6 | "image": "/img/pokemon/pikachu.jpg",
7 | "attacks": {
8 | "special": [
9 | {
10 | "name": "Discharge",
11 | "type": "Electric",
12 | "damage": 35
13 | },
14 | {
15 | "name": "Thunder",
16 | "type": "Electric",
17 | "damage": 100
18 | },
19 | {
20 | "name": "Thunderbolt",
21 | "type": "Electric",
22 | "damage": 55
23 | }
24 | ]
25 | }
26 | },
27 | "mew": {
28 | "id": "UG9rZW1vbjoxNTE=",
29 | "number": "151",
30 | "image": "/img/pokemon/mew.jpg",
31 | "name": "Mew",
32 | "attacks": {
33 | "special": [
34 | {
35 | "name": "Dragon Pulse",
36 | "type": "Dragon",
37 | "damage": 65
38 | },
39 | {
40 | "name": "Earthquake",
41 | "type": "Ground",
42 | "damage": 100
43 | },
44 | {
45 | "name": "Fire Blast",
46 | "type": "Fire",
47 | "damage": 100
48 | },
49 | {
50 | "name": "Hurricane",
51 | "type": "Flying",
52 | "damage": 80
53 | },
54 | {
55 | "name": "Hyper Beam",
56 | "type": "Normal",
57 | "damage": 120
58 | },
59 | {
60 | "name": "Moonblast",
61 | "type": "Fairy",
62 | "damage": 85
63 | },
64 | {
65 | "name": "Psychic",
66 | "type": "Psychic",
67 | "damage": 55
68 | },
69 | {
70 | "name": "Solar Beam",
71 | "type": "Grass",
72 | "damage": 120
73 | },
74 | {
75 | "name": "Thunder",
76 | "type": "Electric",
77 | "damage": 100
78 | }
79 | ]
80 | }
81 | },
82 | "mewtwo": {
83 | "id": "UG9rZW1vbjoxNTA=",
84 | "number": "150",
85 | "image": "/img/pokemon/mewtwo.jpg",
86 | "name": "Mewtwo",
87 | "attacks": {
88 | "special": [
89 | {
90 | "name": "Hyper Beam",
91 | "type": "Normal",
92 | "damage": 120
93 | },
94 | {
95 | "name": "Psychic",
96 | "type": "Psychic",
97 | "damage": 55
98 | },
99 | {
100 | "name": "Shadow Ball",
101 | "type": "Ghost",
102 | "damage": 45
103 | }
104 | ]
105 | }
106 | },
107 | "ditto": {
108 | "id": "UG9rZW1vbjoxMzI=",
109 | "number": "132",
110 | "image": "/img/pokemon/ditto.jpg",
111 | "name": "Ditto",
112 | "attacks": {
113 | "special": [
114 | {
115 | "name": "Struggle",
116 | "type": "Normal",
117 | "damage": 15
118 | }
119 | ]
120 | }
121 | },
122 | "charizard": {
123 | "id": "UG9rZW1vbjowMDY=",
124 | "number": "006",
125 | "name": "Charizard",
126 | "image": "/img/pokemon/charizard.jpg",
127 | "attacks": {
128 | "special": [
129 | {
130 | "name": "Dragon Claw",
131 | "type": "Dragon",
132 | "damage": 35
133 | },
134 | {
135 | "name": "Fire Blast",
136 | "type": "Fire",
137 | "damage": 100
138 | },
139 | {
140 | "name": "Flamethrower",
141 | "type": "Fire",
142 | "damage": 55
143 | }
144 | ]
145 | }
146 | },
147 | "bulbasaur": {
148 | "id": "UG9rZW1vbjowMDE=",
149 | "number": "001",
150 | "name": "Bulbasaur",
151 | "image": "/img/pokemon/bulbasaur.jpg",
152 | "attacks": {
153 | "special": [
154 | {
155 | "name": "Power Whip",
156 | "type": "Grass",
157 | "damage": 70
158 | },
159 | {
160 | "name": "Seed Bomb",
161 | "type": "Grass",
162 | "damage": 40
163 | },
164 | {
165 | "name": "Sludge Bomb",
166 | "type": "Poison",
167 | "damage": 55
168 | }
169 | ]
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/hacks/transactions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "4P812765GHI029827",
4 | "recipient": "mew",
5 | "amount": "$ 15.34",
6 | "message": "Thanks for the salad 🥗"
7 | },
8 | {
9 | "id": "90X21040KL118401T",
10 | "recipient": "charizard",
11 | "amount": "$ 20.00",
12 | "message": "Thanks for the tip about Gyarados 🌊. I never would have won otherwise 😈"
13 | },
14 | {
15 | "id": "89UI190WJJ2240023",
16 | "recipient": "bulbasaur",
17 | "amount": "$ 25.00",
18 | "message": "That play was awesome 🎭. Thanks again for inviting me."
19 | },
20 | {
21 | "id": "0A6FJI65K8173802P",
22 | "recipient": "ditto",
23 | "amount": "$ 12.21",
24 | "message": "blub."
25 | },
26 | {
27 | "id": "9CF911038X034441W",
28 | "recipient": "mewtwo",
29 | "amount": "$ 35.00",
30 | "message": "Still can't believe 🔥 Charizard 🔥 won. That's the last time I bet against him."
31 | },
32 | {
33 | "id": "48L3561JH8132451D",
34 | "recipient": "pikachu",
35 | "amount": "$ 91.10",
36 | "message": "That was ELECTRIC ⚡"
37 | },
38 | {
39 | "id": "6CG59877V61376422",
40 | "recipient": "ditto",
41 | "amount": "$ 98.89",
42 | "message": "blub."
43 | },
44 | {
45 | "id": "U8991IJW02J204032",
46 | "recipient": "bulbasaur",
47 | "amount": "$ 15.98",
48 | "message": "Thanks again for the loan 💵"
49 | },
50 | {
51 | "id": "8XS08JI93J918102S",
52 | "recipient": "charizard",
53 | "amount": "$ 45.14",
54 | "message": "Lunch was delicious, thank you!"
55 | },
56 | {
57 | "id": "21CWW205ND917964J",
58 | "recipient": "mew",
59 | "amount": "$ 12.87",
60 | "message": "Thanks again for the ride 🍃"
61 | }
62 | ]
63 |
--------------------------------------------------------------------------------
/src/hacks/users.json:
--------------------------------------------------------------------------------
1 | {
2 | "pikachu": {
3 | "name": "Pikachu",
4 | "color": "#EDD37E"
5 | },
6 | "mew": {
7 | "name": "Mew",
8 | "color": "#ECC4D0"
9 | },
10 | "mewtwo": {
11 | "name": "Mewtwo",
12 | "color": "#BAABBA"
13 | },
14 | "ditto": {
15 | "name": "Ditto",
16 | "color": "#BDAED1"
17 | },
18 | "charizard": {
19 | "name": "Charizard",
20 | "color": "#EAC492"
21 | },
22 | "bulbasaur": {
23 | "name": "Bulbasaur",
24 | "color": "#7DAD96"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './hacks'
2 | import 'normalize.css/normalize.css'
3 | import './styles.css'
4 | import React from 'react'
5 | import ReactDOM from 'react-dom'
6 | import MainApp from './app'
7 |
8 | const rootEl = document.getElementById('⚛')
9 | const root = ReactDOM.createRoot(rootEl)
10 | root.render()
11 |
12 | // to enable sync mode, comment out the above stuff and comment this in.
13 | // ReactDOM.render(, rootEl)
14 |
--------------------------------------------------------------------------------
/src/load-exercises.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const glob = require('glob')
4 |
5 | const exerciseInfo = glob
6 | .sync('./src/exercises*/[0-9][0-9]*.js')
7 | .reduce((acc, filePath) => {
8 | const contents = String(fs.readFileSync(filePath))
9 | const {dir, name} = path.parse(filePath)
10 | const parentDir = path.basename(dir)
11 | const id = name.split('-')[0]
12 | const number = Number(id)
13 | const previous = String(number - 1).padStart(2, '0')
14 | const next = String(number + 1).padStart(2, '0')
15 | const isExercise = parentDir === 'exercises'
16 | const isExtraCredit = name.includes('-extra')
17 | const isFinal = !isExercise && !isExtraCredit
18 | const [firstLine, secondLine] = contents.split('\n')
19 | const title = firstLine.replace(/\/\//, '').trim()
20 | const extraCreditTitle = secondLine.replace(/\/\/ 💯/, '').trim()
21 | const extraCreditNumber = (name.match(/-extra.(\d+)/) || [])[1]
22 |
23 | acc[id] = acc[id] || {}
24 |
25 | Object.assign(
26 | acc[id],
27 | isExercise
28 | ? {
29 | title,
30 | exercise: {
31 | previous,
32 | next,
33 | isolatedPath: `/isolated/exercises/${name}`,
34 | },
35 | }
36 | : null,
37 | isFinal
38 | ? {
39 | final: {
40 | previous,
41 | next,
42 | isolatedPath: `/isolated/exercises-final/${name}`,
43 | },
44 | }
45 | : null,
46 | isExtraCredit
47 | ? {
48 | extraCreditTitles: {
49 | ...acc[id].extraCreditTitles,
50 | [extraCreditNumber]: extraCreditTitle,
51 | },
52 | }
53 | : null,
54 | )
55 | return acc
56 | }, {})
57 |
58 | // get rid of next and previous which don't exist
59 | for (const infoKey in exerciseInfo) {
60 | const info = exerciseInfo[infoKey]
61 | if (!exerciseInfo[info.exercise.previous]) {
62 | delete info.exercise.previous
63 | }
64 | if (!exerciseInfo[info.exercise.next]) {
65 | delete info.exercise.next
66 | }
67 | if (!exerciseInfo[info.final.previous]) {
68 | delete info.final.previous
69 | }
70 | if (!exerciseInfo[info.final.next]) {
71 | delete info.final.next
72 | }
73 | }
74 |
75 | module.exports = exerciseInfo
76 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect'
2 |
3 | afterEach(() => {
4 | jest.clearAllMocks()
5 | })
6 |
7 | jest.spyOn(window, 'alert').mockImplementation(() => {})
8 |
9 | // none of these tests should actually invoke fetch
10 | beforeEach(() => {
11 | jest.spyOn(window, 'fetch').mockImplementation((...args) => {
12 | console.warn('window.fetch is not mocked for this call', ...args)
13 | return Promise.reject(new Error('This must be mocked!'))
14 | })
15 | })
16 |
17 | afterEach(() => {
18 | window.fetch.mockRestore()
19 | })
20 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | a {
2 | color: #cc0000;
3 | }
4 |
5 | a:focus,
6 | a:hover,
7 | a:active {
8 | color: #8a0000;
9 | }
10 |
11 | input {
12 | line-height: 2;
13 | font-size: 16px;
14 | box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
15 | border: none;
16 | border-radius: 2px;
17 | padding-left: 10px;
18 | padding-right: 10px;
19 | background-color: #eee;
20 | }
21 |
22 | button {
23 | font-size: 1rem;
24 | font-family: inherit;
25 | border: 1px solid #ff0000;
26 | background-color: #cc0000;
27 | cursor: pointer;
28 | padding: 8px 10px;
29 | color: #eee;
30 | border-radius: 3px;
31 | }
32 |
33 | button:disabled {
34 | border-color: #dc9494;
35 | background-color: #f16161;
36 | cursor: unset;
37 | }
38 |
39 | button:hover:not(:disabled),
40 | button:active:not(:disabled),
41 | button:focus:not(:disabled) {
42 | border-color: #cc0000;
43 | background-color: #8a0000;
44 | }
45 |
46 | .isolated-top-container {
47 | padding: 30px;
48 | height: 100vh;
49 | display: grid;
50 | align-items: center;
51 | justify-content: center;
52 | }
53 |
54 | .totally-centered {
55 | width: 100%;
56 | height: 100%;
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | }
61 |
62 | .pokemon-form {
63 | display: flex;
64 | flex-direction: column;
65 | align-items: center;
66 | }
67 |
68 | .pokemon-form input {
69 | margin-top: 10px;
70 | margin-right: 10px;
71 | }
72 |
73 | .pokemon-info {
74 | height: 400px;
75 | width: 300px;
76 | overflow: scroll;
77 | background-color: #eee;
78 | border-radius: 4px;
79 | padding: 10px;
80 | position: relative;
81 | }
82 |
83 | .pokemon-info.pokemon-loading {
84 | opacity: 0.6;
85 | transition: opacity 0s;
86 | /* note: the transition delay is the same as the busyDelayMs config */
87 | transition-delay: 0.4s;
88 | }
89 |
90 | .pokemon-info h2 {
91 | font-weight: bold;
92 | text-align: center;
93 | margin-top: 0.3em;
94 | }
95 |
96 | .pokemon-info img {
97 | max-width: 100%;
98 | max-height: 200px;
99 | }
100 |
101 | .pokemon-info .pokemon-info__img-wrapper {
102 | text-align: center;
103 | margin-top: 20px;
104 | }
105 |
106 | .pokemon-info .pokemon-info__fetch-time {
107 | position: absolute;
108 | top: 6px;
109 | right: 10px;
110 | }
111 |
112 | button.invisible-button {
113 | border: none;
114 | padding: inherit;
115 | font-size: inherit;
116 | font-family: inherit;
117 | cursor: pointer;
118 | font-weight: inherit;
119 | background-color: transparent;
120 | color: #000;
121 | }
122 | button.invisible-button:hover,
123 | button.invisible-button:active,
124 | button.invisible-button:focus {
125 | border: none;
126 | background-color: transparent;
127 | }
128 |
--------------------------------------------------------------------------------
/src/suspense-list/app.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | background-color: #eee;
3 | min-height: 100%;
4 | }
5 |
6 | .mainContentArea {
7 | display: flex;
8 | justify-content: space-between;
9 | }
10 |
11 | .mainContentArea > *:not(:last-child) {
12 | margin-right: 20px;
13 | }
14 |
15 | .mainContentArea > *:not(:first-child) {
16 | margin-left: 20px;
17 | }
18 |
19 | .spinnerContainer {
20 | flex: 1;
21 | padding-top: 10px;
22 | }
23 |
--------------------------------------------------------------------------------
/src/suspense-list/img.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {createResource} from '../utils'
3 |
4 | function preloadImage(src) {
5 | return new Promise(resolve => {
6 | const img = document.createElement('img')
7 | img.src = src
8 | img.onload = () => resolve(src)
9 | })
10 | }
11 |
12 | const imgSrcResourceCache = {}
13 |
14 | function Img({src, alt, ...props}) {
15 | let imgSrcResource = imgSrcResourceCache[src]
16 | if (!imgSrcResource) {
17 | imgSrcResource = createResource(() => preloadImage(src))
18 | imgSrcResourceCache[src] = imgSrcResource
19 | }
20 | return
21 | }
22 |
23 | export default Img
24 |
--------------------------------------------------------------------------------
/src/suspense-list/left-nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as cn from './left-nav.module.css'
3 |
4 | function LeftNav() {
5 | return (
6 |