├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── index.js └── lessons │ ├── 101 │ ├── README.md │ ├── app.js │ └── pokemon-detail.js │ ├── 102 │ ├── README.md │ ├── app.js │ └── pokemon-detail.js │ ├── 103 │ ├── README.md │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 104 │ ├── README.md │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 105 │ ├── README.md │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 106 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 107 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 108 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 109 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 110 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 111 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ └── pokemon-detail.js │ ├── 112 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 113 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 114 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 115 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 116 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 117 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 201 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 202 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 203 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 204 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ ├── 205 │ ├── README.md │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ └── ui.js │ └── complete │ ├── api.js │ ├── app.js │ ├── error-boundary.js │ ├── pokemon-detail.js │ ├── pokemon.js │ ├── styles.css │ └── ui.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # project 26 | /src/lessons/build-an-app-with-react-suspense 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | © 2019 Michael Chan 2 | This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build an App with React Suspense 2 | 3 | Welcome! 4 | 5 | We're going to have a great time exploring React's experimental Concurrent Mode. 6 | Concurrent Mode is an exciting leap forward in user interface development. 7 | It allows us to describe user interface transitions in beautiful detail. 8 | 9 | Apps aren't static. 10 | They move — responding to user input as they dart back and forth to the internet for data. Concurrent Mode and Suspense give us a language for those movements. 11 | 12 | In this workshop we talk project setup, finding help, and what to expect from this very exciting — very experimental — feature. 13 | 14 | This course is narrowly focused on Concurrent Mode and Suspense. 15 | We will not cover adjacent topics like styling or code organization. 16 | 17 | ## On Egghead 18 | 19 | This course is available to egghead.io members or as a standalone course for \$99. 20 | 21 | [Buy the course or membership here](https://egghead.io/courses/build-an-app-with-react-suspense?af=1x80ad) 22 | 23 | ## Contact 24 | 25 | If you have questions, hit me up on twitter — [@chantastic](https://twitter.com/chantastic) — or via email (in my [Github bio](https://github.com/chantastic/)). 26 | 27 | ## WARNING!!! 28 | 29 | What we'll cover here is not "stable" React API. 30 | It will likely change. 31 | 32 | ## Who is this for? 33 | 34 | This is an advanced React course. 35 | I assume that you've built applications in React. 36 | 37 | You could absolutely follow this tutorial with no React experience. 38 | However, the patterns I'll demonstrate here won't feel necessary unless you've worked in a complex codebase. 39 | 40 | The goal of this course isn't to teach React from scratch 41 | But to showcase this glimpse of the future we have from React. 42 | 43 | ## Why should you care? 44 | 45 | Suspense will change the way you interact with data. 46 | It also changes the way you control visual transitions between pending and loaded states. 47 | 48 | ## What's the goal of this course? 49 | 50 | My goal is that you walk into Suspense with a good understanding of the underlying principles. 51 | We'll start with an introduction to the basics of Suspense. 52 | We'll close with general component design and organization that I think will help you, regardless of Suspense. 53 | 54 | ## Installation and usage 55 | 56 | This is a standard `create-react-app` application. 57 | 58 | - Clone the repo 59 | - CD into the project directory (`react-suspense-course` by default) 60 | - Run `yarn` to install the deps 61 | - Run `yarn start` to start a development server 62 | 63 | ## Course organization 64 | 65 | - `src/index` is the main app 66 | - Change lessons by changing the imported directory 67 | - `src/lessons/` are all the workshop lessons 68 | - lessons are chronological 69 | - each lesson has a `README.md` with a lesson description and assignment 70 | - the solution to each lesson is the next lesson 71 | - `complete` is the completed application with everything in place 72 | 73 | © 2019 Michael Chan 74 | 75 | Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-an-app-react-suspense-hooks-and-context", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^0.0.0-experimental-b53ea6ca0", 7 | "react-dom": "^0.0.0-experimental-b53ea6ca0", 8 | "react-scripts": "3.2.0", 9 | "sleep-promise": "^8.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chantastic/react-suspense-course/4f63306a253ef3ff630e7ad80ee04bd6db3fedf4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chantastic/react-suspense-course/4f63306a253ef3ff630e7ad80ee04bd6db3fedf4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chantastic/react-suspense-course/4f63306a253ef3ff630e7ad80ee04bd6db3fedf4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | // ↓↓↓ 👋 Update this line to change the lesson number ↓↓↓ 5 | import Lesson from "./lessons/complete/app"; 6 | 7 | function App() { 8 | return ; 9 | } 10 | 11 | const rootElement = document.getElementById("root"); 12 | 13 | // ReactDOM.render(, rootElement); // Blocking Mode 14 | ReactDOM.createRoot(rootElement).render(); // Concurrent Mode 15 | -------------------------------------------------------------------------------- /src/lessons/101/README.md: -------------------------------------------------------------------------------- 1 | # Import Components Lazily with Suspense React.lazy 2 | 3 | The `Suspense` component isn't new. 4 | We've been able to use it since 2018. 5 | 6 | Learn the simplest way to start using Suspense in any React codebase (v16.6 or later), using dynamic imports. 7 | 8 | ## Video 9 | 10 | [At egghead.io](https://egghead.io/lessons/react-import-components-lazily-with-suspense-react-lazy?af=1x80ad) 11 | 12 | ## Exercise 13 | 14 | ### 1. Convert import syntax from Static to Dynamic 15 | 16 | ```diff 17 | // app.js 18 | 19 | - import PokemonDetail from "./pokemon-detail"; 20 | + const PokemonDetail = import("./pokemon-detail"); 21 | ``` 22 | 23 | ### 2. Wrap the import in React.lazy() so React knows how to handle promise status 24 | 25 | ```diff 26 | // app.js 27 | 28 | - const PokemonDetail = import("./pokemon-detail"); 29 | + const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 30 | ``` 31 | 32 | ### 3. Wrap Imported Component in Suspense with `fallback` prop 33 | 34 | ```diff 35 | // app.js 36 | 37 | - 38 | + Fetching Pokemon..."}> 39 | + 40 | + 41 | ``` 42 | 43 | We can look in the network panel and see that this chunk of code is now being asyncronously loaded — and no longer in our application bundle. 44 | 45 | If it's hard to see, you can change the network speed to see, you can change the network speed. 46 | Or use the React Dev Tools to Suspend that component — we'll talk more about devtools later in the course 47 | 48 | ## Summary 49 | 50 | Congrats! You've used Suspense to dynamically import a component and manage the transition from loading message to loaded component. 51 | 52 | ## Solution 53 | 54 | Lesson [102](../102) holds the solution to this lesson. 55 | -------------------------------------------------------------------------------- /src/lessons/101/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // 1. Change this static import to a dynamic import, wrapped in React.lazy 3 | import PokemonDetail from "./pokemon-detail"; 4 | 5 | export default function App() { 6 | return ( 7 |
8 | {/* 2. Wrap this component in a React.Suspense component with fallback */} 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/lessons/101/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function PokemonDetail() { 4 | return
Static Pokemon
; 5 | } 6 | -------------------------------------------------------------------------------- /src/lessons/102/README.md: -------------------------------------------------------------------------------- 1 | # Catch Errors with an Error Boundary Component 2 | 3 | JavaScript's try/catch feature allows you to isolate errors and prevent them from halting execution in adjacent parts of an app. 4 | 5 | Error boundaries do the same for component trees. 6 | They allow you to isolate errors and send them to an error reporting service. 7 | 8 | Let's learn they are used to catch errors thrown by promises. 9 | 10 | ## Video 11 | 12 | [On egghead.io](https://egghead.io/lessons/react-catch-errors-with-an-error-boundary-component?af=1x80ad) 13 | 14 | ## Exercise 15 | 16 | ### 1. Temporarily create an import error 17 | 18 | ```diff 19 | // app.js 20 | 21 | - const PokemonDetail = React.lazy(() => Promise.reject()); 22 | + const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 23 | ``` 24 | 25 | ### 2. Read the React's console error 26 | 27 | React's errors are great. 28 | They'll tell you to go here to read more on setting up an `ErrorBoundary` component. 29 | 30 | ### 3. Copy/Paste React's generic ErrorBoundary into the app 31 | 32 | ```diff 33 | // app.js 34 | 35 | + class ErrorBoundary extends React.Component { 36 | + constructor(props) { 37 | + super(props); 38 | + this.state = { hasError: false }; 39 | + } 40 | + 41 | + static getDerivedStateFromError(error) { 42 | + return { hasError: true }; 43 | + } 44 | + 45 | + componentDidCatch(error, errorInfo) { 46 | + logErrorToMyService(error, errorInfo); 47 | + } 48 | + 49 | + render() { 50 | + if (this.state.hasError) { 51 | + return

Something went wrong.

; 52 | + } 53 | + 54 | + return this.props.children; 55 | + } 56 | + } 57 | ``` 58 | 59 | ### 4. Connect or remove `logErrorToMyService` call 60 | 61 | For the purpose of this course, log errors directly to the console. 62 | 63 | ```diff 64 | componentDidCatch(error, errorInfo) { 65 | - logErrorToMyService(error, errorInfo); 66 | + console.error(error, errorInfo); 67 | } 68 | ``` 69 | 70 | ### 5. Add a `fallback=` prop like the one Suspense has 71 | 72 | ```diff 73 | + static defaultProps = { 74 | + fallback:

Something went wrong.

75 | + }; 76 | 77 | render() { 78 | if (this.state.hasError) { 79 | - return

Something went wrong.

; 80 | + return this.props.fallback; 81 | } 82 | 83 | return this.props.children; 84 | } 85 | ``` 86 | 87 | ### 6. Wrap your Suspense Component in the new ErrorBoundary component to catch and isolate import errors 88 | 89 | ```diff 90 | + Couldn't catch 'em all.}> 91 | 92 | 93 | 94 | + 95 | ``` 96 | 97 | ## Summary 98 | 99 | You've just added an `ErrorBoundary` component. 100 | 101 | You'll find that you only really need one of these in an application. 102 | 103 | So as an extra credit, move this component into a file of its own. 104 | 105 | ## Solution 106 | 107 | Lesson [103](../103) holds the solution to this lesson. 108 | -------------------------------------------------------------------------------- /src/lessons/102/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // 1. Create an import error by giving React.lazy a Promise.reject() 3 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 4 | 5 | // 2. Check the console for errors about error boundaries 6 | 7 | // 3-5. Copy/Paste an ErrorBoundary declaration here and configuer with a fallback= prop 8 | // https://reactjs.org/docs/error-boundaries.html 9 | 10 | export default function App() { 11 | return ( 12 |
13 | {/* 6. Wrap your Suspense Component in the freshly minted ErrorBoundary component */} 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/lessons/102/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function PokemonDetail() { 4 | return
Static Pokemon
; 5 | } 6 | -------------------------------------------------------------------------------- /src/lessons/103/README.md: -------------------------------------------------------------------------------- 1 | # Understand How React.lazy Communicates Loading Status to Suspense and Error Boundaries 2 | 3 | Suspense won't magically detect and inspect promises in your code. 4 | You have to wrap promises to communicate status to Suspense and error boundaries. 5 | 6 | `React.lazy` acts as a model for which states we need our promise wrappers to handle. 7 | 8 | ## Video 9 | 10 | [On egghead.io](https://egghead.io/lessons/react-understand-how-react-lazy-communicates-loading-status-to-suspense-and-error-boundaries?af=1x80ad) 11 | 12 | ## Exercise 13 | 14 | With our Suspense and ErrorBoundary fallbacks in place, 15 | let's break our component import and force the three possible states that we're now setup to handle. 16 | 17 | ### 1: Error 18 | 19 | If the module fails to load for some reason, it will be picked up by our `` 20 | 21 | Give it a try with: 22 | 23 | ```js 24 | const PokemonDetail = React.lazy(() => Promise.reject()); 25 | ``` 26 | 27 | ### 2. Pending 28 | 29 | While our module awaits the network, it will be picked up by our `` 30 | 31 | Give it a try with: 32 | 33 | ```js 34 | const PokemonDetail = React.lazy( 35 | () => new Promise(resolve => setTimeout(resolve, 1000)) 36 | ); 37 | ``` 38 | 39 | ### 3. Success 40 | 41 | When our module successfully loads (after a short delay), the component will be rendered. 42 | 43 | Give it a try with: 44 | 45 | ```js 46 | const PokemonDetail = React.lazy( 47 | () => 48 | new Promise(resolve => 49 | setTimeout( 50 | () => resolve({ default: () =>
Fake Pokemon
}), 51 | 2000 52 | ) 53 | ) 54 | ); 55 | ``` 56 | 57 | ...or, just put it back to fix the import :) 58 | 59 | `const PokemonDetail = React.lazy(() => import("./pokemon-detail"))` 60 | 61 | ## Put everything back 62 | 63 | And that's what happens when we is dynamic import to load components 64 | 65 | --- 66 | 67 | ## Solution 68 | 69 | Lesson [104](../104) holds the solution to this lesson. 70 | -------------------------------------------------------------------------------- /src/lessons/103/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | // Test the three states we're setup for 5 | // 1. Error: return a rejected promise to React.lazy to enact the ErrorBoundary fallback 6 | // 2. Pending: return a new pending promise to React.lazy to enact the Suspense fallback 7 | // 3. Success: return a new pending promise that resolves a module after a timeout to React.lazy to enact the Suspense fallback 8 | // Or just fix it to import properly `pokemon-default` module. 9 | const PokemonDetail = React.lazy(() => Promise.reject()); 10 | 11 | export default function App() { 12 | return ( 13 |
14 |

Pokedex

15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/lessons/103/error-boundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // https://reactjs.org/docs/error-boundaries.html 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/103/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function PokemonDetail() { 4 | return
Static Pokemon
; 5 | } 6 | -------------------------------------------------------------------------------- /src/lessons/104/README.md: -------------------------------------------------------------------------------- 1 | # Wrap Fetch Requests to Communicate Pending, Error and Success Status to Suspense 2 | 3 | Like `React.lazy`, we can write promise wrappers — of our own — to communicate pending, error, and success statuses to `Suspense` and error boundaries components. 4 | 5 | The wrapper we write in this lesson is the minimum viable wrapper required for data fetching. 6 | It isn't a robust data solution. 7 | However, knowing how to wrap promises for communication with `Suspense` and error boundaries allows you to suspend any asynchronous data. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-wrap-fetch-requests-to-communicate-pending-error-and-success-status-to-suspense?af=1x80ad) 12 | 13 | ## Exercise 14 | 15 | ### 1. Add a the baseline "suspensify" function 16 | 17 | _WARNING:_ 18 | This is a minimal reference for how to wrap your promises in Suspense. 19 | 20 | ```diff 21 | + function suspensify(promise) { 22 | + let status = "pending"; 23 | + let result; 24 | + let suspender = promise.then( 25 | + response => { 26 | + status = "success"; 27 | + result = response; 28 | + }, 29 | + error => { 30 | + status = "error"; 31 | + result = error; 32 | + } 33 | + ); 34 | + 35 | + return { 36 | + read() { 37 | + if (status === "pending") { 38 | + throw suspender; 39 | + } 40 | + if (status === "error") { 41 | + throw result; 42 | + } 43 | + if (status === "success") { 44 | + return result; 45 | + } 46 | + } 47 | + }; 48 | + } 49 | ``` 50 | 51 | ### 2. Fetch a Pokemon from Pokeapi 52 | 53 | ```diff 54 | + let pokemon = fetch(`https://pokeapi.co/api/v2/pokemon/1`).then(res => res.json()) 55 | ``` 56 | 57 | ### 3. Wrap the fetch request in the `suspensify` function 58 | 59 | ```diff 60 | - let pokemon = fetch(`https://pokeapi.co/api/v2/pokemon/1`).then(res => res.json()) 61 | + let pokemon = suspensify(fetch(`https://pokeapi.co/api/v2/pokemon/1`).then(res => res.json())); 62 | ``` 63 | 64 | ### 4. Call `pokemon.read()` in PokemonDetail to access data 65 | 66 | ```diff 67 | - return
Static Pokemon
; 68 | + return
{pokemon.read().name}
; 69 | ``` 70 | 71 | ## Solution 72 | 73 | Lesson [105](../105) holds the solution to this lesson. 74 | -------------------------------------------------------------------------------- /src/lessons/104/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/104/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/104/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // 1. Uncomment this baseline `suspensify` function for communicating promise status to React 4 | // function suspensify(promise) { 5 | // let status = "pending"; 6 | // let result; 7 | // let suspender = promise.then( 8 | // response => { 9 | // status = "success"; 10 | // result = response; 11 | // }, 12 | // error => { 13 | // status = "error"; 14 | // result = error; 15 | // } 16 | // ); 17 | 18 | // return { 19 | // read() { 20 | // if (status === "pending") { 21 | // } 22 | // if (status === "error") { 23 | // throw result; 24 | // } 25 | // if (status === "success") { 26 | // return result; 27 | // } 28 | // } 29 | // }; 30 | // } 31 | 32 | // 2. Fetch a pokemon from PokeAPI and store `pokemon` variable 33 | // fetch(`https://pokeapi.co/api/v2/pokemon/1`) 34 | // 3. Wrap this fetch request in the `suspensify` function 35 | 36 | export default function PokemonDetail() { 37 | // 4. `pokemon` is now a resource with a `read()` function on it 38 | // use `{pokemon.read().name}` to display the name of the first Pokemon fetched from the internet 39 | return
Static Pokemon
; 40 | } 41 | -------------------------------------------------------------------------------- /src/lessons/105/README.md: -------------------------------------------------------------------------------- 1 | # Track Async Requests with React's useState Hook 2 | 3 | The `useState` Hook is the best way to track state in React. 4 | It's capabilities aren't limited to known values either. 5 | State can be set with asynchronously resolved values as well — like the result of a fetch request. 6 | Wrapped promises can be given to `useState` to communicate promise status for state transitions. 7 | 8 | ## Video 9 | 10 | [On egghead.io](https://egghead.io/lessons/react-track-async-requests-with-react-s-usestate-hook?af=1x80ad) 11 | 12 | ## Exercise 13 | 14 | ### 1. Use React.useState to track the current Pokemon resource 15 | 16 | ```diff 17 | // pokemon-detail.js #PokemonDetail 18 | + let [pokemonResource, setPokemonResource] = React.useState(); 19 | ``` 20 | 21 | ### 2. Rename `pokemon` to `initialPokemon` to indicate that it's only the initial Pokemon 22 | 23 | ```diff 24 | - let pokemon = suspensify(fetchPokemon(1)); 25 | + let initialPokemon = suspensify(fetchPokemon(1)); 26 | ``` 27 | 28 | ### 3. Provide `initialPokemon` to `React.useState` as default 29 | 30 | ```diff 31 | // pokemon-detail.js #PokemonDetail 32 | - let [pokemonResource, setPokemonResource] = React.useState(); 33 | + let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 34 | ``` 35 | 36 | ### 4. Create an intermediate `pokemon` variable that `read()`s the `pokemonResource` 37 | 38 | ```diff 39 | // pokemon-detail.js #PokemonDetail 40 | + let pokemon = pokemonResource.read(); 41 | ``` 42 | 43 | ### 5. Add a "Next" button to shuttle thru Pokemon 44 | 45 | ```diff 46 | // pokemon-detail.js #PokemonDetail 47 | 48 | + 56 | ``` 57 | 58 | ## Solution 59 | 60 | Lesson [106](../106) holds the solution to this lesson. 61 | -------------------------------------------------------------------------------- /src/lessons/105/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/105/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/105/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function suspensify(promise) { 4 | let status = "pending"; 5 | let result; 6 | let suspender = promise.then( 7 | response => { 8 | status = "success"; 9 | result = response; 10 | }, 11 | error => { 12 | status = "error"; 13 | result = error; 14 | } 15 | ); 16 | 17 | return { 18 | read() { 19 | if (status === "pending") { 20 | throw suspender; 21 | } 22 | if (status === "error") { 23 | throw result; 24 | } 25 | if (status === "success") { 26 | return result; 27 | } 28 | } 29 | }; 30 | } 31 | 32 | // 2. Rename the `pokemon` variable to `initialPokemon` to indicate that it is only the first 33 | let pokemon = suspensify( 34 | fetch(`https://pokeapi.co/api/v2/pokemon/1`).then(res => res.json()) 35 | ); 36 | 37 | export default function PokemonDetail() { 38 | // 1. Use React.useState to track the current PokemonResource and setPokemonResource 39 | // 2. (see above) 40 | // 3. Provide `initialPokemon` to `React.useState` as default 41 | // 4. Create an intermediate `pokemon` variable that `read()`s the `pokemonResource` 42 | // 5. Create "Next" button 43 | // * When clicked, call `setPokemonResource` 44 | // * Use suspensify(fetchPokemon(...)) to fetch the pokemon with the next id 45 | return
{pokemon.read().name}
; 46 | } 47 | -------------------------------------------------------------------------------- /src/lessons/106/README.md: -------------------------------------------------------------------------------- 1 | # Separate API Utility Functions from Components 2 | 3 | Separating concerns can improve code re-use. 4 | Keeping API functions separate from components can be a nice way to isolate concerns with the data layer and make data fetching functions available to other modules. 5 | 6 | ## Video 7 | 8 | [On egghead.io](https://egghead.io/lessons/react-separate-api-utility-functions-from-components?af=1x80ad) 9 | 10 | ## Exercise 11 | 12 | Now that we have real data, let's add a next button to this component. 13 | 14 | We need a place to store the date. 15 | React.useState is great for that. 16 | 17 | Let's put the pokemon we've got into state. 18 | And since we'll have next pokemon, 19 | Let's call this `initialPokemon` 20 | 21 | Now we hookup an onClick 22 | Which — when clicked — will set our Pokemon 23 | Here we'll just give it a suspensified function to fetch the next pokemon 24 | 25 | ## Solution 26 | 27 | Lesson [107](../107) holds the solution to this lesson. 28 | -------------------------------------------------------------------------------- /src/lessons/106/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/106/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/106/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/106/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | let initialPokemon = suspensify(fetchPokemon(1)); 5 | 6 | export default function PokemonDetail() { 7 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 8 | let pokemon = pokemonResource.read(); 9 | 10 | return ( 11 |
12 |
{pokemon.name}
13 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/lessons/107/README.md: -------------------------------------------------------------------------------- 1 | # Enable Suspense Features with Experimental Concurrent Mode using ReactDOM.createRoot 2 | 3 | Concurrent Mode is a completely different rendering paradigm for React. 4 | It changes something that has remained constant since the first version of React: `ReactDOM.render`. 5 | 6 | To use Concurrent Mode, we use `ReactDom.createRoot`. 7 | It's API is slightly different then the old faithful `ReactDOM.render` but, with this one change, we can access the future of React. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-enable-suspense-features-with-experimental-concurrent-mode-using-reactdom-createroot?af=1x80ad) 12 | 13 | ## Exercise 14 | 15 | everything from this point forward requires the experimental build 16 | 17 | ```diff 18 | - ReactDOM.render(, rootElement); 19 | + ReactDOM.createRoot(rootElement).render(); 20 | ``` 21 | 22 | Different modes: https://reactjs.org/docs/concurrent-mode-adoption.html#feature-comparison 23 | 24 | ## Solution 25 | 26 | Lesson [108](../108) holds the solution to this lesson. 27 | -------------------------------------------------------------------------------- /src/lessons/107/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/107/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/107/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/107/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | let initialPokemon = suspensify(fetchPokemon(1)); 5 | 6 | export default function PokemonDetail() { 7 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 8 | let pokemon = pokemonResource.read(); 9 | 10 | return ( 11 |
12 |
{pokemon.name}
13 | 14 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/lessons/108/README.md: -------------------------------------------------------------------------------- 1 | # De-prioritize Non User-Blocking Updates with useTransition's startTransition function 2 | 3 | In blocking rendering, all updates have the same priority. 4 | 5 | In Concurrent Mode, work is "interruptible". 6 | User-blocking updates are treated with the highest importance. 7 | 8 | To keep interfaces interactive and snappy, we can de-prioritize slower updates. 9 | 10 | The `useTransition` Hook allows React to schedule work after higher priority work. 11 | 12 | ## Video 13 | 14 | [On egghead.io](https://egghead.io/lessons/react-de-prioritize-non-user-blocking-updates-with-usetransition-s-starttransition-function?af=1x80ad) 15 | 16 | ## Exercise 17 | 18 | ### 1. Destructure `startTransition` from `React.useTransition` 19 | 20 | ```diff 21 | // pokemon-detail.js #PokemonDetail 22 | 23 | + let [startTransition] = React.useTransition(); 24 | ``` 25 | 26 | ### 2. Wrap the `setPokemonResource` onClick handler in `startTransition` 27 | 28 | ```diff 29 | // pokemon-detail.js #PokemonDetail 30 | - setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1))) 31 | + startTransition(() => 32 | + setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1))) 33 | + ) 34 | ``` 35 | 36 | ## Solution 37 | 38 | Lesson [109](../109) holds the solution to this lesson. 39 | -------------------------------------------------------------------------------- /src/lessons/108/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/108/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/108/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/108/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | let initialPokemon = suspensify(fetchPokemon(1)); 5 | 6 | export default function PokemonDetail() { 7 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 8 | // 1. Destructure `startTransition` from `React.useTransition` 9 | let pokemon = pokemonResource.read(); 10 | 11 | return ( 12 |
13 |
{pokemon.name}
14 | 15 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/lessons/109/README.md: -------------------------------------------------------------------------------- 1 | # Bypass Receded Views with useTransition's timeoutMs option 2 | 3 | Suspense components know one thing — how to show a fallback when promises are pending. 4 | As new data is requested in these Suspense boundaries, the previous data will be replaced with the fallback. 5 | 6 | This is re-presentation of the fallback state is known as the "receded state". 7 | 8 | We can configure `useTransition` to present the the previous rendering of the component for a specified duration with the `timeoutMs' option. 9 | 10 | ## Video 11 | 12 | [On egghead.io](https://egghead.io/lessons/react-bypass-receded-views-with-usetransition-s-timeoutms-option?af=1x80ad) 13 | 14 | ## Exercise 15 | 16 | We have this component, 17 | We're using useTransition to deprioritize prevent our suspensified state update block user updates 18 | 19 | But we have a problem. 20 | Let's slow things down to see it. 21 | 22 | When we click the Next button, our entire app goes back to the loading state. 23 | This is pretty jarring. 24 | 25 | React calls this view the "Receeded" state. 26 | 27 | Our component is fetching the next component and therefor Suspends, 28 | rendering the nearest fallback. 29 | 30 | So they've given us a mechanism to bypass this receeded state. 31 | 32 | useTransition takes an options object. 33 | We can pass `timeoutMs` a period of time we're willing to see the previous state before transitioning to the next view. 34 | This is a maximum. 35 | So if the requist resolves in less time, we won't be left waiting the full time specified. 36 | 37 | Now, when we click next, the previous state sticks around. 38 | 39 | If we slow things down, we see that the transition will happen (ready or not) after the specified wait time. 40 | But an fast speeds, we won't see the receeded state. 41 | 42 | ## Solution 43 | 44 | Lesson [110](../110) holds the solution to this lesson. 45 | -------------------------------------------------------------------------------- /src/lessons/109/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/109/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/109/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/109/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | /* 5 | ## title: reduce the priority of async state changes with useTransition 6 | 7 | As we click this button — in concurrent mode — requesting the next pokemon, we get this funny new error 8 | 9 | ``` 10 | Warning: PokemonDetail triggered a user-blocking update that suspended. 11 | 12 | The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. 13 | 14 | Refer to the documentation for useTransition to learn how to implement this pattern. 15 | ``` 16 | 17 | The first thing we get from useTransition is a function. 18 | This function is used to wrap suspendible state updates. 19 | 20 | Wrapping our setPokemonResource call in startTransition, 21 | we communicate to React that this update is lower prior. 22 | 23 | Also, it just makes the error go away :) 24 | */ 25 | 26 | let initialPokemon = suspensify(fetchPokemon(1)); 27 | 28 | export default function PokemonDetail() { 29 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 30 | let [startTransition] = React.useTransition(); 31 | let pokemon = pokemonResource.read(); 32 | 33 | return ( 34 |
35 |
{pokemon.name}
36 | 37 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/lessons/110/README.md: -------------------------------------------------------------------------------- 1 | # Display Loading States Conditionally with React.useTransition's isPending Boolean 2 | 3 | It's a good practice to give users immediate feedback while asynchronous work is being completed. 4 | 5 | `useTransition` returns a boolean we can use to conditionally render loading UI. 6 | This boolean lives the second element in the array returned by `useTransitions`. 7 | By convention, it's assigned to a variable named `isPending`. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-display-loading-states-conditionally-with-react-usetransition-s-ispending-boolean?af=1x80ad) 12 | 13 | ## Exercise 14 | 15 | useTransition provides a second argument. 16 | by convention, assigned as isPending. 17 | 18 | we can use to provide immediate feedback to a user that work is happening. 19 | 20 | Let's use it to disable the button, as clicking it again might just delay things further. 21 | 22 | And for the fun of it, let's conditionally show an emoji spinner. 23 | 24 | Because this is quick and dirty, we can use a style tag to keep all this right inline. 25 | 26 | ## Solution 27 | 28 | Lesson [111](../111) holds the solution to this lesson. 29 | -------------------------------------------------------------------------------- /src/lessons/110/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/110/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/110/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/110/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | let initialPokemon = suspensify(fetchPokemon(1)); 5 | 6 | export default function PokemonDetail() { 7 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 8 | let [startTransition] = React.useTransition({ timeoutMs: 1000 }); 9 | let pokemon = pokemonResource.read(); 10 | 11 | return ( 12 |
13 |
{pokemon.name}
14 | 15 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/lessons/111/README.md: -------------------------------------------------------------------------------- 1 | # Delay the Appearance of a Loading Spinner with CSS 2 | 3 | Eager delay spinners are not a good user experience. 4 | They can make a snappy user interface feel slower. 5 | 6 | We want delay spinners to appear only after a perceivable delay. 7 | `useTransition` doesn't yet have an API for customizing this. 8 | Until it does, we can use CSS animations to delay visibility of delay spinners. 9 | 10 | ## Exercise 11 | 12 | We have here a component that show ... 13 | And it shows a DelaySpinner that is really helpful with slow internet speeds. 14 | But when we speed it up, we see this unsettling effect. 15 | 16 | The spinner pops in for a split second with EVERY SINGLE REQUEST. 17 | That's not necessry or pleasent. 18 | 19 | Again, we can fix this and React has a great article on this technique on the Concurrent docs — doc name. 20 | 21 | describe 22 | 23 | So let's copy it and add it to our own component. 24 | 25 | Show online, fast, and slow 26 | 27 | Now that this is fleshed out. 28 | Let's move it into a new module for shared ui. 29 | 30 | ## Video 31 | 32 | [On egghead.io](https://egghead.io/lessons/react-delay-the-appearance-of-a-loading-spinner-with-css?af=1x80ad) 33 | 34 | # Exercise 35 | 36 | ## Fine-tuning interactions 37 | 38 | ... and now that i have these intermediate states in place, i'm willing to wait a little longer before seeing the receeded state. 39 | let's bump it up to somethig like 3 seconds 40 | 41 | let's also add a spinner on the next page to make that look live as well 42 | 43 | move into UI 44 | 45 | ## Solution 46 | 47 | Lesson [112](../112) holds the solution to this lesson. 48 | -------------------------------------------------------------------------------- /src/lessons/111/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/111/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/111/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/111/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchPokemon, suspensify } from "./api"; 3 | 4 | let initialPokemon = suspensify(fetchPokemon(1)); 5 | 6 | function DelaySpinner() { 7 | return ( 8 | 9 | 21 | 🌀 22 | 23 | ); 24 | } 25 | 26 | export default function PokemonDetail() { 27 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 28 | let [startTransition, isPending] = React.useTransition({ timeoutMs: 1000 }); 29 | let pokemon = pokemonResource.read(); 30 | 31 | return ( 32 |
33 |
{pokemon.name}
34 | 35 | 46 | 47 | {isPending && } 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/lessons/112/README.md: -------------------------------------------------------------------------------- 1 | # Get Previous Resource Values with React’s useDeferredState Hook 2 | 3 | The `useDeferredValue` Hook gives us a way to hold onto a previous resource values while waiting for a new one. 4 | 5 | This is a more hands-on alternative to the magic of `useTransition`. 6 | With `useTransition`, React "keeps" the previous rendering and gives you a magical `isPending` boolean to conditionally show loading UI. 7 | 8 | `useDeferredValue` puts you in the driver seat by giving you the actual value. 9 | This value can be used to implement our own `isPending`. 10 | 11 | ## Video 12 | 13 | [On egghead.io](https://egghead.io/lessons/react-get-previous-resource-values-with-react-s-usedeferredstate-hook?af=1x80ad) 14 | 15 | ## Exercise 16 | 17 | ### 1. Define `deferredPokemonResource` using `React.useDeferredValue(PokemonResource) 18 | 19 | ```diff 20 | // pokemon-detail.js 21 | + let deferredPokemon = React.useDeferredValue(PokemonResource); 22 | ``` 23 | 24 | ### 2. Provide the `timeoutMs` option to `React.useDeferredValue` as the second argument 25 | 26 | ```diff 27 | // pokemon-detail.js 28 | - let deferredPokemon = React.useDeferredValue(PokemonResource); 29 | + let deferredPokemon = React.useDeferredValue(PokemonResource, { timeoutMs: 3000 }); 30 | ``` 31 | 32 | ### 3. Remove `isPending` from `React.useTransition` 33 | 34 | ```diff 35 | // pokemon-detail.js 36 | - let [startTransition, isPending] = React.useTransition(); 37 | + let [startTransition] = React.useTransition(); 38 | ``` 39 | 40 | ### 4. Define `isPending` ensuring that `PokemonResource` and `deferredPokemonResource` are different 41 | 42 | ```diff 43 | // app.js 44 | + let isPending = deferredPokemonResource !== pokemonResource; 45 | ``` 46 | 47 | ## Solution 48 | 49 | Lesson [113](../113) holds the solution to this lesson. 50 | -------------------------------------------------------------------------------- /src/lessons/112/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/112/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/lessons/112/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/112/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | import { fetchPokemon, suspensify } from "./api"; 4 | 5 | let initialPokemon = suspensify(fetchPokemon(1)); 6 | 7 | export default function PokemonDetail() { 8 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 9 | let [startTransition, isPending] = React.useTransition({ timeoutMs: 1000 }); 10 | let pokemon = pokemonResource.read(); 11 | 12 | // EXERCISE 13 | // 1. Define `deferredPokemonResource` using `React.useDeferredValue(PokemonResource) 14 | // 2. Provide the `timeoutMs` option to `React.useDeferredValue` as the second argument 15 | // 3. Remove `isPending` from `React.useTransition` 16 | // 4. Define `isPending` by comparing `PokemonResource` and `deferredPokemonResource` — ensuring that they are different 17 | 18 | return ( 19 |
20 |
21 | {pokemon.name} {isPending && } 22 |
23 | 24 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/lessons/112/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/113/README.md: -------------------------------------------------------------------------------- 1 | # Hoist Component State 2 | 3 | Hoisting state is a common Refactoring in React. 4 | State in one component may need to be lifted up to a parent component for coordination with other component state. 5 | 6 | This can be an error-prone refactoring. 7 | With a good editor, it's best to start with the returned JSX and move out. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-hoist-component-state?af=1x80ad) 12 | 13 | ## Solution 14 | 15 | Lesson [114](../114) holds the solution to this lesson. 16 | -------------------------------------------------------------------------------- /src/lessons/113/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/113/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | 4 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Pokedex

10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lessons/113/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/113/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | import { fetchPokemon, suspensify } from "./api"; 4 | 5 | let initialPokemon = suspensify(fetchPokemon(1)); 6 | 7 | export default function PokemonDetail() { 8 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 9 | let [startTransition, isPending] = React.useTransition({ timeoutMs: 3000 }); 10 | let pokemon = pokemonResource.read(); 11 | 12 | return ( 13 |
14 |
{pokemon.name}
15 | 16 | 27 | 28 | {isPending && } 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/lessons/113/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/114/README.md: -------------------------------------------------------------------------------- 1 | # Avoid this Common Suspense Gotcha by Reading Data From Components 2 | 3 | Suspense can have an unfriendly learning curve. 4 | Components with suspended content need a component boundary. 5 | Resource reads can't happen in the same component as the `Suspense` and error boundaries components. 6 | 7 | When you have your Suspense and error boundary components in place but still get errors about them absence, you probably need to move a `read()` call into a component. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-avoid-this-common-suspense-gotcha-by-reading-data-from-components?af=1x80ad) 12 | 13 | ## Solution 14 | 15 | Lesson [115](../115) holds the solution to this lesson. 16 | -------------------------------------------------------------------------------- /src/lessons/114/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function suspensify(promise) { 8 | let status = "pending"; 9 | let result; 10 | let suspender = promise.then( 11 | response => { 12 | status = "success"; 13 | result = response; 14 | }, 15 | error => { 16 | status = "error"; 17 | result = error; 18 | } 19 | ); 20 | 21 | return { 22 | read() { 23 | if (status === "pending") { 24 | throw suspender; 25 | } 26 | if (status === "error") { 27 | throw result; 28 | } 29 | if (status === "success") { 30 | return result; 31 | } 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lessons/114/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | 9 | export default function App() { 10 | let [pokemon, setPokemon] = React.useState(initialPokemon); 11 | let deferredPokemon = React.useDeferredValue(pokemon, { 12 | timeoutMs: 3000 13 | }); 14 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 15 | let [startTransition] = React.useTransition(); 16 | 17 | return ( 18 |
19 |

Pokedex

20 | 21 | 22 | 23 | 27 | 28 | 41 | 42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/lessons/114/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

Something went wrong.

12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/114/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
9 |
10 | {pokemon.name} 11 | {isStale && } 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/114/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/115/README.md: -------------------------------------------------------------------------------- 1 | # Coordinate Fallback rendering with the SuspenseList Component 2 | 3 | `SuspenseList` is how React coordinates the reveal order of `Suspense` components. 4 | It only accepts `Suspense` components as children — which can effect where you error boundaries are placed. 5 | 6 | By default `SuspenseList` will show suspended component fallbacks together and reveal `children` as suspenders resolve. 7 | 8 | ## Video 9 | 10 | [On egghead.io](https://egghead.io/lessons/react-coordinate-fallback-rendering-with-the-suspenselist-component?af=1x80ad) 11 | 12 | ## Solution 13 | 14 | [Lesson 116](../116) is holds the solution to this lesson. 15 | -------------------------------------------------------------------------------- /src/lessons/115/api.js: -------------------------------------------------------------------------------- 1 | export function fetchPokemon(id) { 2 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => 3 | res.json() 4 | ); 5 | } 6 | 7 | export function fetchPokemonCollection() { 8 | return fetch(`https://pokeapi.co/api/v2/pokemon`).then(res => res.json()); 9 | } 10 | 11 | export function suspensify(promise) { 12 | let status = "pending"; 13 | let result; 14 | let suspender = promise.then( 15 | response => { 16 | status = "success"; 17 | result = response; 18 | }, 19 | error => { 20 | status = "error"; 21 | result = error; 22 | } 23 | ); 24 | 25 | return { 26 | read() { 27 | if (status === "pending") { 28 | throw suspender; 29 | } 30 | if (status === "error") { 31 | throw result; 32 | } 33 | if (status === "success") { 34 | return result; 35 | } 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/lessons/115/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection() { 11 | return ( 12 |
13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • {pokemon.name}
  • 15 | ))} 16 |
    17 | ); 18 | } 19 | 20 | export default function App() { 21 | let [pokemon, setPokemon] = React.useState(initialPokemon); 22 | let deferredPokemon = React.useDeferredValue(pokemon, { 23 | timeoutMs: 3000 24 | }); 25 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 26 | let [startTransition] = React.useTransition(); 27 | 28 | return ( 29 |
    30 |

    Pokedex

    31 | 32 | 33 | 34 | 38 | 39 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
    61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/lessons/115/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/115/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/115/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/116/README.md: -------------------------------------------------------------------------------- 1 | # Reveal Suspense Components in Order with SuspenseList's revealOrder Prop 2 | 3 | `revealOrder` is one of `SuspenseList`s configuration options. It can be undefined, `together`, `forwards`, and `backwards`. 4 | 5 | - `undefined` (default): reveal children as suspenders resolve 6 | - `together`: reveal children together, once all suspenders are resolved 7 | - `forwards`: render children from top to bottom, indifferent to suspender resolution order 8 | - `backwards`: render children from bottom to top, indifferent to suspender resolution order 9 | 10 | ## Video 11 | 12 | [On egghead.io](https://egghead.io/lessons/react-reveal-suspense-components-in-order-with-suspenselist-s-revealorder-prop?af=1x80ad) 13 | 14 | ## Solution 15 | 16 | [Lesson 117](../117) is holds the solution to this lesson. 17 | -------------------------------------------------------------------------------- /src/lessons/116/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(2000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(sleep(1000)); 13 | } 14 | 15 | export function suspensify(promise) { 16 | let status = "pending"; 17 | let result; 18 | let suspender = promise.then( 19 | response => { 20 | status = "success"; 21 | result = response; 22 | }, 23 | error => { 24 | status = "error"; 25 | result = error; 26 | } 27 | ); 28 | 29 | return { 30 | read() { 31 | if (status === "pending") { 32 | throw suspender; 33 | } 34 | if (status === "error") { 35 | throw result; 36 | } 37 | if (status === "success") { 38 | return result; 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/lessons/116/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection() { 11 | return ( 12 |
    13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • {pokemon.name}
  • 15 | ))} 16 |
    17 | ); 18 | } 19 | 20 | export default function App() { 21 | let [pokemon, setPokemon] = React.useState(initialPokemon); 22 | let deferredPokemon = React.useDeferredValue(pokemon, { 23 | timeoutMs: 3000 24 | }); 25 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 26 | let [startTransition] = React.useTransition(); 27 | 28 | return ( 29 |
    30 |

    Pokedex

    31 | 32 | 33 | Fetching Pokemon...
    }> 34 | 35 | 39 | 40 | 53 | 54 | 55 | 56 | Fetching the Database...}> 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/lessons/116/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/116/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/116/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/117/README.md: -------------------------------------------------------------------------------- 1 | # Avoid Too Many Spinners with SuspenseList’s tail Prop 2 | 3 | `tail` is an optional prop for `SuspenseList`. 4 | It works in tandem with `revealOrder` and has three options: `undefined`, `collapsed`, and `hidden`. 5 | 6 | These options can be used to configure how fallbacks are displayed. 7 | 8 | - `undefined`: show all `fallbacks` 9 | - `hidden`: show no `fallbacks` 10 | - `collapsed`: show only the next `fallback` 11 | 12 | ## Video 13 | 14 | [On egghead.io](https://egghead.io/lessons/react-avoid-too-many-spinners-with-suspenselist-s-tail-prop?af=1x80ad) 15 | 16 | ## Solution 17 | 18 | Lesson [201](../201) holds the solution to this lesson. 19 | -------------------------------------------------------------------------------- /src/lessons/117/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(1000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(sleep(2000)); 13 | } 14 | 15 | export function suspensify(promise) { 16 | let status = "pending"; 17 | let result; 18 | let suspender = promise.then( 19 | response => { 20 | status = "success"; 21 | result = response; 22 | }, 23 | error => { 24 | status = "error"; 25 | result = error; 26 | } 27 | ); 28 | 29 | return { 30 | read() { 31 | if (status === "pending") { 32 | throw suspender; 33 | } 34 | if (status === "error") { 35 | throw result; 36 | } 37 | if (status === "success") { 38 | return result; 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/lessons/117/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection() { 11 | return ( 12 |
    13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • {pokemon.name}
  • 15 | ))} 16 |
    17 | ); 18 | } 19 | 20 | export default function App() { 21 | let [pokemon, setPokemon] = React.useState(initialPokemon); 22 | let deferredPokemon = React.useDeferredValue(pokemon, { 23 | timeoutMs: 3000 24 | }); 25 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 26 | let [startTransition] = React.useTransition(); 27 | 28 | return ( 29 |
    30 |

    Pokedex

    31 | 32 | 33 | Fetching Pokemon...
    }> 34 | 35 | 39 | 40 | 53 | 54 | 55 | 56 | Fetching the Database...}> 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/lessons/117/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/117/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/117/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/201/README.md: -------------------------------------------------------------------------------- 1 | # Pass Components a useTransition-Wrapped, State-Setting, Callback 2 | 3 | The `useState` functions we wrap in `useTransition` function wrappers can be passed around to components like any callback. 4 | Instead of making all of your components aware of Concurrent Mode, you can provide wrapped callbacks and continue compatability with both legacy and future React code. 5 | 6 | ## Video 7 | 8 | [On egghead.io](https://egghead.io/lessons/react-pass-components-a-usetransition-wrapped-state-setting-callback?af=1x80ad) 9 | 10 | ## Solution 11 | 12 | Lesson [202](../202) holds the solution to this lesson. 13 | -------------------------------------------------------------------------------- /src/lessons/201/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(1000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(sleep(2000)); 13 | } 14 | 15 | export function suspensify(promise) { 16 | let status = "pending"; 17 | let result; 18 | let suspender = promise.then( 19 | response => { 20 | status = "success"; 21 | result = response; 22 | }, 23 | error => { 24 | status = "error"; 25 | result = error; 26 | } 27 | ); 28 | 29 | return { 30 | read() { 31 | if (status === "pending") { 32 | throw suspender; 33 | } 34 | if (status === "error") { 35 | throw result; 36 | } 37 | if (status === "success") { 38 | return result; 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/lessons/201/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection() { 11 | return ( 12 |
    13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • {pokemon.name}
  • 15 | ))} 16 |
    17 | ); 18 | } 19 | 20 | export default function App() { 21 | let [pokemon, setPokemon] = React.useState(initialPokemon); 22 | let deferredPokemon = React.useDeferredValue(pokemon, { 23 | timeoutMs: 3000 24 | }); 25 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 26 | let [startTransition] = React.useTransition(); 27 | 28 | return ( 29 |
    30 |

    Pokedex

    31 | 32 | 33 | Fetching Pokemon...
    }> 34 | 35 | 39 | 40 | 53 | 54 | 55 | 56 | Fetching the Database...}> 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/lessons/201/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/201/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/201/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/202/README.md: -------------------------------------------------------------------------------- 1 | # Augment Resource JSON with Custom Properties 2 | 3 | You're never stuck with the data you get from the server. 4 | The Pokeapi doesn't have an id property on Pokemon but we can transform our response to include exactly what we need. 5 | 6 | With a little destructuring assignment and object spread we can augment our API with helpful properties. 7 | 8 | ## Video 9 | 10 | [On egghead.io](https://egghead.io/lessons/react-augment-resource-json-with-custom-properties?af=1x80ad) 11 | 12 | ## Solution 13 | 14 | Lesson [203](../203) holds the solution to this lesson. 15 | -------------------------------------------------------------------------------- /src/lessons/202/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(1000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(sleep(2000)); 13 | } 14 | 15 | export function suspensify(promise) { 16 | let status = "pending"; 17 | let result; 18 | let suspender = promise.then( 19 | response => { 20 | status = "success"; 21 | result = response; 22 | }, 23 | error => { 24 | status = "error"; 25 | result = error; 26 | } 27 | ); 28 | 29 | return { 30 | read() { 31 | if (status === "pending") { 32 | throw suspender; 33 | } 34 | if (status === "error") { 35 | throw result; 36 | } 37 | if (status === "success") { 38 | return result; 39 | } 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/lessons/202/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection({ onClick }) { 11 | return ( 12 |
    13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • 15 | 21 |
  • 22 | ))} 23 |
    24 | ); 25 | } 26 | 27 | export default function App() { 28 | let [pokemon, setPokemon] = React.useState(initialPokemon); 29 | let deferredPokemon = React.useDeferredValue(pokemon, { 30 | timeoutMs: 3000 31 | }); 32 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 33 | let [startTransition] = React.useTransition(); 34 | 35 | return ( 36 |
    37 |

    Pokedex

    38 | 39 | 40 | Fetching Pokemon...
    }> 41 | 42 | 46 | 47 | 60 | 61 | 62 | 63 | Fetching the Database...}> 64 | 65 | 67 | startTransition(() => setPokemon(suspensify(fetchPokemon(id)))) 68 | } 69 | /> 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/lessons/202/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/202/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/202/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/203/README.md: -------------------------------------------------------------------------------- 1 | # Extract Reusable Components with an As Prop, Render Props, and React.Fragment 2 | 3 | React is about re-useable components. 4 | Often we put to many opinions into our components and diminish that re-useability. 5 | 6 | `React.Fragment`, an `as` component prop, a `renderItem` render prop, JSX spread attributes, and object default values are tools you can use to make truly re-useable list components. 7 | 8 | ## Video 9 | 10 | [On egghead.io](https://egghead.io/lessons/react-extract-reusable-components-with-an-as-prop-render-props-and-react-fragment?af=1x80ad) 11 | 12 | ## Solution 13 | 14 | Lesson [204](../204) holds the solution to this lesson. 15 | -------------------------------------------------------------------------------- /src/lessons/203/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(1000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(res => ({ 13 | ...res, 14 | results: res.results.map(pokemon => ({ 15 | ...pokemon, 16 | id: pokemon.url.split("/")[6] 17 | })) 18 | })) 19 | .then(sleep(2000)); 20 | } 21 | 22 | export function suspensify(promise) { 23 | let status = "pending"; 24 | let result; 25 | let suspender = promise.then( 26 | response => { 27 | status = "success"; 28 | result = response; 29 | }, 30 | error => { 31 | status = "error"; 32 | result = error; 33 | } 34 | ); 35 | 36 | return { 37 | read() { 38 | if (status === "pending") { 39 | throw suspender; 40 | } 41 | if (status === "error") { 42 | throw result; 43 | } 44 | if (status === "success") { 45 | return result; 46 | } 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/lessons/203/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection({ onClick }) { 11 | return ( 12 |
    13 | {initialCollection.read().results.map(pokemon => ( 14 |
  • 15 | 18 |
  • 19 | ))} 20 |
    21 | ); 22 | } 23 | 24 | export default function App() { 25 | let [pokemon, setPokemon] = React.useState(initialPokemon); 26 | let deferredPokemon = React.useDeferredValue(pokemon, { 27 | timeoutMs: 3000 28 | }); 29 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 30 | let [startTransition] = React.useTransition(); 31 | 32 | return ( 33 |
    34 |

    Pokedex

    35 | 36 | 37 | Fetching Pokemon...
    }> 38 | 39 | 43 | 44 | 57 | 58 | 59 | 60 | Fetching the Database...}> 61 | 62 | 64 | startTransition(() => setPokemon(suspensify(fetchPokemon(id)))) 65 | } 66 | /> 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/lessons/203/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/203/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/203/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/204/README.md: -------------------------------------------------------------------------------- 1 | # Connect a New Endpoints in a Suspense App 2 | 3 | Our app has evolved. 4 | It doesn't make sense to scrub thru Pokemon one at a time when we have a list. 5 | Let's change the function of our "Next" button to fetch the next page of Pokemon results by adding a new endpoint and connecting it to Suspense transitions. 6 | 7 | ## Video 8 | 9 | [On egghead.io](https://egghead.io/lessons/react-connect-a-new-endpoints-in-a-suspense-app?af=1x80ad) 10 | 11 | ## Solution 12 | 13 | Lesson [205](../205) holds the solution to this lesson. 14 | -------------------------------------------------------------------------------- /src/lessons/204/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(1000)); 7 | } 8 | 9 | export function fetchPokemonCollection() { 10 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 11 | .then(res => res.json()) 12 | .then(res => ({ 13 | ...res, 14 | results: res.results.map(pokemon => ({ 15 | ...pokemon, 16 | id: pokemon.url.split("/")[6] 17 | })) 18 | })) 19 | .then(sleep(2000)); 20 | } 21 | 22 | export function suspensify(promise) { 23 | let status = "pending"; 24 | let result; 25 | let suspender = promise.then( 26 | response => { 27 | status = "success"; 28 | result = response; 29 | }, 30 | error => { 31 | status = "error"; 32 | result = error; 33 | } 34 | ); 35 | 36 | return { 37 | read() { 38 | if (status === "pending") { 39 | throw suspender; 40 | } 41 | if (status === "error") { 42 | throw result; 43 | } 44 | if (status === "success") { 45 | return result; 46 | } 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/lessons/204/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | 5 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 6 | 7 | let initialPokemon = suspensify(fetchPokemon(1)); 8 | let initialCollection = suspensify(fetchPokemonCollection()); 9 | 10 | function PokemonCollection(props) { 11 | return ; 12 | } 13 | 14 | function List({ 15 | as: As = React.Fragment, 16 | items = [], 17 | renderItem = item =>
    {item.name}
    18 | }) { 19 | return {items.map(renderItem)}; 20 | } 21 | 22 | export default function App() { 23 | let [pokemon, setPokemon] = React.useState(initialPokemon); 24 | let deferredPokemon = React.useDeferredValue(pokemon, { 25 | timeoutMs: 3000 26 | }); 27 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 28 | let [startTransition] = React.useTransition(); 29 | 30 | return ( 31 |
    32 |

    Pokedex

    33 | 34 | 35 | Fetching Pokemon...
    }> 36 | 37 | 41 | 42 | 55 | 56 | 57 | 58 | Fetching the Database...}> 59 | 60 | ( 62 |
  • 63 | 73 |
  • 74 | )} 75 | /> 76 |
    77 |
    78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/lessons/204/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/204/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/204/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/205/README.md: -------------------------------------------------------------------------------- 1 | # Provide Suspensified Data to Components with Context Providers, Consumers, and useContext 2 | 3 | Proper Suspense code can mean a lot of functions wrapped in other functions. 4 | Because these function are composed with Hooks, modules can't help us hide the implementation details. 5 | But Context can! 6 | 7 | Let's explore how Context Providers, Consumers, and the `useContext` Hook can integrate with Suspense to mask wordy `useTransition`-wrapped API calls. 8 | 9 | ## Video 10 | 11 | [On egghead.io](https://egghead.io/lessons/react-provide-suspensified-data-to-components-with-context-providers-consumers-and-usecontext?af=1x80ad) 12 | 13 | ## Solution 14 | 15 | Lesson ["complete"](../complete) holds the solution to this lesson. 16 | -------------------------------------------------------------------------------- /src/lessons/205/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function fetchPokemon(id) { 4 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 5 | .then(res => res.json()) 6 | .then(sleep(500)); 7 | } 8 | 9 | export function fetchPokemonCollectionUrl(url) { 10 | return fetch(url) 11 | .then(res => res.json()) 12 | .then(res => ({ 13 | ...res, 14 | results: res.results.map(pokemon => ({ 15 | ...pokemon, 16 | id: pokemon.url.split("/")[6] 17 | })) 18 | })) 19 | .then(sleep(1000)); 20 | } 21 | 22 | export function fetchPokemonCollection() { 23 | return fetch(`https://pokeapi.co/api/v2/pokemon`) 24 | .then(res => res.json()) 25 | .then(res => ({ 26 | ...res, 27 | results: res.results.map(pokemon => ({ 28 | ...pokemon, 29 | id: pokemon.url.split("/")[6] 30 | })) 31 | })) 32 | .then(sleep(1000)); 33 | } 34 | 35 | export function suspensify(promise) { 36 | let status = "pending"; 37 | let result; 38 | let suspender = promise.then( 39 | response => { 40 | status = "success"; 41 | result = response; 42 | }, 43 | error => { 44 | status = "error"; 45 | result = error; 46 | } 47 | ); 48 | 49 | return { 50 | read() { 51 | if (status === "pending") { 52 | throw suspender; 53 | } 54 | if (status === "error") { 55 | throw result; 56 | } 57 | if (status === "success") { 58 | return result; 59 | } 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/lessons/205/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { DelaySpinner } from "./ui"; 4 | import { 5 | fetchPokemon, 6 | fetchPokemonCollectionUrl, 7 | fetchPokemonCollection, 8 | suspensify 9 | } from "./api"; 10 | 11 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 12 | 13 | let initialPokemon = suspensify(fetchPokemon(1)); 14 | let initialCollection = suspensify(fetchPokemonCollection()); 15 | 16 | function PokemonCollection({ resource, ...props }) { 17 | return ; 18 | } 19 | 20 | function List({ 21 | as: As = React.Fragment, 22 | items = [], 23 | renderItem = item =>
    {item.name}
    24 | }) { 25 | return {items.map(renderItem)}; 26 | } 27 | 28 | export default function App() { 29 | let [pokemon, setPokemon] = React.useState(initialPokemon); 30 | let [collection, setCollection] = React.useState(initialCollection); 31 | let deferredPokemon = React.useDeferredValue(pokemon, { 32 | timeoutMs: 3000 33 | }); 34 | let deferredPokemonIsStale = deferredPokemon !== pokemon; 35 | let [startTransition, isPending] = React.useTransition({ timeoutMs: 3000 }); 36 | 37 | return ( 38 |
    39 |

    Pokedex

    40 | 41 | 42 | Fetching Pokemon...
    }> 43 | 44 | 48 | 49 | 50 | 51 | Fetching the Database...}> 52 | 53 |
    54 | 69 | {isPending && } 70 |
    71 | 72 | ( 75 |
  • 76 | 87 |
  • 88 | )} 89 | /> 90 |
    91 |
    92 | 93 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/lessons/205/error-boundary.js: -------------------------------------------------------------------------------- 1 | // copied from https://reactjs.org/docs/error-boundaries.html 2 | import React from "react"; 3 | 4 | export default class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false }; 8 | } 9 | 10 | static defaultProps = { 11 | fallback:

    Something went wrong.

    12 | }; 13 | 14 | static getDerivedStateFromError(error) { 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | console.log(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | return this.props.fallback; 25 | } 26 | 27 | return this.props.children; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lessons/205/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | 4 | export default function PokemonDetail({ resource, isStale }) { 5 | let pokemon = resource.read(); 6 | 7 | return ( 8 |
    9 |
    10 | {pokemon.name} 11 | {isStale && } 12 |
    13 |
    14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lessons/205/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 25 | 🌀 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/lessons/complete/api.js: -------------------------------------------------------------------------------- 1 | import sleep from "sleep-promise"; 2 | 3 | export function suspensify(promise) { 4 | let status = "pending"; 5 | let result; 6 | let suspender = promise.then( 7 | response => { 8 | status = "success"; 9 | result = response; 10 | }, 11 | error => { 12 | status = "error"; 13 | result = error; 14 | } 15 | ); 16 | 17 | return { 18 | read() { 19 | if (status === "pending") throw suspender; 20 | if (status === "error") throw result; 21 | if (status === "success") return result; 22 | } 23 | }; 24 | } 25 | 26 | export function fetchPokemon(id) { 27 | return fetch(`https://pokeapi.co/api/v2/pokemon/${id}`) 28 | .then(res => res.json()) 29 | .then(sleep(500)); 30 | } 31 | 32 | export function fetchPokemonCollection() { 33 | return fetch(`https://pokeapi.co/api/v2/pokemon/`) 34 | .then(res => res.json()) 35 | .then(res => ({ 36 | ...res, 37 | results: res.results.map(pokemon => ({ 38 | ...pokemon, 39 | id: pokemon.url.split("/")[6] 40 | })) 41 | })) 42 | .then(sleep(1000)); 43 | } 44 | 45 | export function fetchPokemonCollectionUrl(url) { 46 | return fetch(url) 47 | .then(res => res.json()) 48 | .then(res => ({ 49 | ...res, 50 | results: res.results.map(pokemon => ({ 51 | ...pokemon, 52 | id: pokemon.url.split("/")[6] 53 | })) 54 | })) 55 | .then(sleep(1000)); 56 | } 57 | -------------------------------------------------------------------------------- /src/lessons/complete/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "./error-boundary"; 3 | import { fetchPokemon, fetchPokemonCollection, suspensify } from "./api"; 4 | import { List } from "./ui"; 5 | import { PokemonContext } from "./pokemon"; 6 | 7 | import "./styles.css"; 8 | 9 | const PokemonDetail = React.lazy(() => import("./pokemon-detail")); 10 | 11 | let initialPokemon = suspensify(fetchPokemon(1)); 12 | let initialCollection = suspensify(fetchPokemonCollection()); 13 | 14 | export default function App() { 15 | let [pokemonResource, setPokemonResource] = React.useState(initialPokemon); 16 | let [collectionResource] = React.useState(initialCollection); 17 | let [startTransition, isPending] = React.useTransition({ timeoutMs: 3000 }); 18 | let deferredPokemonResource = React.useDeferredValue(pokemonResource, { 19 | timeoutMs: 3000 20 | }); 21 | 22 | let pokemonIsPending = deferredPokemonResource !== pokemonResource; 23 | 24 | let pokemonState = { 25 | pokemon: deferredPokemonResource, 26 | isStale: pokemonIsPending, 27 | setPokemon: id => 28 | startTransition(() => setPokemonResource(suspensify(fetchPokemon(id)))) 29 | }; 30 | 31 | return ( 32 |
    33 |
    34 | 35 | 36 | Fetching Pokemon stats...
    }> 37 | 38 | 39 | 40 | 41 | 42 | Connecting to database...}> 43 | 44 | {/*
    45 | 63 | 64 | {isPending && } 65 |
    */} 66 |
    67 |
    68 | 69 | {({ setPokemon }) => ( 70 | ( 75 |
  • 76 | 89 |
  • 90 | )} 91 | /> 92 | )} 93 |
    94 |
    95 |
    96 | 97 | 98 | 99 | ); 100 | } 101 | 102 | function PokemonCollection({ resource, ...props }) { 103 | return ; 104 | } 105 | -------------------------------------------------------------------------------- /src/lessons/complete/error-boundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static defaultProps = { 10 | fallback:
    Something went wrong.
    11 | }; 12 | 13 | static getDerivedStateFromError(error) { 14 | // Update state so the next render will show the fallback UI. 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | // You can also log the error to an error reporting service 20 | console.error(error, errorInfo); 21 | } 22 | 23 | render() { 24 | if (this.state.hasError) { 25 | // You can render any custom fallback UI 26 | return this.props.fallback; 27 | } 28 | 29 | return this.props.children; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lessons/complete/pokemon-detail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DelaySpinner } from "./ui"; 3 | import { PokemonContext } from "./pokemon"; 4 | 5 | export default function PokemonDetail() { 6 | let { pokemon: resource, isStale } = React.useContext(PokemonContext); 7 | let pokemon = resource.read(); 8 | 9 | function TypeItem({ style, ...props }) { 10 | return ( 11 |
  • 23 | ); 24 | } 25 | 26 | function PoisonTypeItem(props) { 27 | return ; 28 | } 29 | 30 | function GrassTypeItem(props) { 31 | return ( 32 | 33 | ); 34 | } 35 | 36 | function WaterTypeItem(props) { 37 | return ; 38 | } 39 | 40 | function FireTypeItem(props) { 41 | return ; 42 | } 43 | 44 | return ( 45 |
    46 |
    47 | {pokemon.name} 52 | 53 |
    54 |

    55 | {pokemon.name} {isStale && } 56 |

    57 | 58 |
    59 |

    type:

    60 |
      61 | {pokemon.types.map(({ type }) => { 62 | switch (type.name) { 63 | case "grass": 64 | return {type.name}; 65 | case "poison": 66 | return {type.name}; 67 | case "water": 68 | return {type.name}; 69 | case "fire": 70 | return {type.name}; 71 | default: 72 | return {type.name}; 73 | } 74 | })} 75 |
    76 |
    77 |
    78 |
    79 | 80 |
    81 |
    82 |
    83 | {pokemon.height} 84 | Height 85 |
    86 |
    87 | {pokemon.weight} 88 | Weight 89 |
    90 |
    91 | {pokemon.abilities.map(({ ability }, i) => ( 92 | 93 | {ability.name.replace("-", " ")} 94 | {i !== pokemon.abilities.length - 1 && ", "} 95 | 96 | ))} 97 | Abilities 98 |
    99 |
    100 |
    101 | 102 |
    103 |

    Stats

    104 | 105 |
    106 | {pokemon.stats.map(({ base_stat, stat }) => ( 107 |
    108 | {base_stat} 109 | {stat.name.replace("-", " ")} 110 |
    111 | ))} 112 |
    113 |
    114 |
    115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/lessons/complete/pokemon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const PokemonContext = React.createContext(); 4 | -------------------------------------------------------------------------------- /src/lessons/complete/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | /* outline: 1px solid #cecece; */ 4 | } 5 | 6 | body { 7 | --background: #fafafa; 8 | --border-radius-m: 5px; 9 | --shadow-m: 0 7px 34px 0 hsl(0, 0%, 91%), 0 3px 6px 0 hsl(0, 0%, 95%); 10 | --text: #232323; 11 | background: var(--background); 12 | color: var(--text); 13 | font-family: system-ui, sans-serif; 14 | margin: 0; 15 | overflow-y: scroll; 16 | padding: 0; 17 | } 18 | 19 | .title { 20 | align-items: center; 21 | display: flex; 22 | justify-content: center; 23 | text-align: center; 24 | } 25 | 26 | .container { 27 | align-items: flex-start; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: flex-start; 31 | margin: 0 auto; 32 | max-width: 640px; 33 | padding: 0 10px 50px 10px; 34 | } 35 | 36 | button { 37 | cursor: pointer; 38 | } 39 | 40 | ul { 41 | margin: 0; 42 | padding: 0; 43 | } 44 | 45 | section, 46 | article { 47 | width: 100%; 48 | } 49 | 50 | section > h2 { 51 | text-align: center; 52 | } 53 | 54 | article { 55 | background: white; 56 | border-radius: var(--border-radius-m); 57 | box-shadow: var(--shadow-m); 58 | padding: 24px; 59 | padding-top: 12px; 60 | } 61 | 62 | /* ———————————— POKEMON INDEX ———————————— */ 63 | 64 | .pokemon-list { 65 | display: grid; 66 | grid-gap: 10px; 67 | grid-template-columns: repeat(auto-fill, minmax(179px, 1fr)); 68 | text-transform: capitalize; 69 | width: 100%; 70 | } 71 | 72 | .pokemon-list-item { 73 | align-items: center; 74 | background: white; 75 | border-radius: var(--border-radius-m); 76 | border: 1px solid white; 77 | box-shadow: var(--shadow-m); 78 | cursor: pointer; 79 | list-style-type: none; 80 | } 81 | 82 | .pokemon-list-item-button { 83 | align-items: center; 84 | background: white; 85 | border-radius: var(--border-radius-m); 86 | border: 1px solid white; 87 | box-shadow: var(--shadow-m); 88 | cursor: pointer; 89 | display: flex; 90 | padding: 10px; 91 | width: 100%; 92 | 93 | border: none; 94 | -webkit-appearance: none; 95 | appearance: none; 96 | } 97 | 98 | .pokemon-list-item > img { 99 | margin-right: 0.5rem; 100 | } 101 | 102 | .pokemon-list-item:hover { 103 | background: #fafafa; 104 | } 105 | 106 | /* ———————————— POKEMON DETAIL ———————————— */ 107 | 108 | .button-back { 109 | background: transparent; 110 | border: none; 111 | font-size: 1rem; 112 | margin-bottom: 8px; 113 | } 114 | 115 | .detail-header { 116 | align-items: center; 117 | display: flex; 118 | } 119 | 120 | @media only screen and (max-width: 600px) { 121 | .detail-header { 122 | align-items: center; 123 | display: flex; 124 | flex-direction: column; 125 | } 126 | } 127 | 128 | .detail-header > img { 129 | margin-right: 1rem; 130 | } 131 | 132 | .pokemon-title { 133 | font-size: 3rem; 134 | margin: 0; 135 | padding: 0; 136 | text-transform: capitalize; 137 | } 138 | 139 | .pokemon-type-container { 140 | align-items: center; 141 | display: flex; 142 | } 143 | 144 | .pokemon-type-container > h4 { 145 | font-weight: 300; 146 | margin-right: 8px; 147 | } 148 | 149 | /* ——————— POKEMON STATS */ 150 | 151 | .stats-grid { 152 | border: 1px solid #f3f3f3; 153 | display: grid; 154 | grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); 155 | text-align: center; 156 | width: 100%; 157 | } 158 | 159 | .stat-item { 160 | align-items: center; 161 | border: 1px solid #f3f3f3; 162 | display: flex; 163 | flex-direction: column; 164 | justify-content: center; 165 | padding: 30px 10px; 166 | } 167 | 168 | .stat-header { 169 | font-size: 1.5rem; 170 | font-weight: 500; 171 | } 172 | 173 | .stat-header-long { 174 | font-size: 1rem; 175 | font-weight: 600; 176 | } 177 | 178 | .stat-body { 179 | font-size: 0.85rem; 180 | margin-top: 8px; 181 | opacity: 0.8; 182 | text-transform: uppercase; 183 | } 184 | -------------------------------------------------------------------------------- /src/lessons/complete/ui.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function DelaySpinner() { 4 | return ( 5 | 6 | 24 | 🌀 25 | 26 | ); 27 | } 28 | 29 | export function List({ 30 | as: As = React.Fragment, 31 | items = [], 32 | renderItem = item =>
    {item.name}
    , 33 | ...props 34 | }) { 35 | return {items.map(renderItem)}; 36 | } 37 | --------------------------------------------------------------------------------