17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/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/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/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 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/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/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 |
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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/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/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 |