`
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 | +
51 | + setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
52 | + }
53 | + >
54 | + Next
55 | +
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 |
16 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
17 | }
18 | >
19 | Next
20 |
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 |
17 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
18 | }
19 | >
20 | Next
21 |
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 |
18 | // 2. Wrap the `setPokemonResource` onClick handler in `startTransition`
19 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
20 | }
21 | >
22 | Next
23 |
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 |
40 | startTransition(() =>
41 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
42 | )
43 | }
44 | >
45 | Next
46 |
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 |
18 | startTransition(() =>
19 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
20 | )
21 | }
22 | >
23 | Next
24 |
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 |
39 | startTransition(() =>
40 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
41 | )
42 | }
43 | >
44 | Next
45 |
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 |
28 | startTransition(() =>
29 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
30 | )
31 | }
32 | >
33 | Next
34 |
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 |
20 | startTransition(() =>
21 | setPokemonResource(suspensify(fetchPokemon(pokemon.id + 1)))
22 | )
23 | }
24 | >
25 | Next
26 |
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 |
32 | startTransition(() =>
33 | setPokemon(
34 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
35 | )
36 | )
37 | }
38 | >
39 | Next
40 |
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 |
43 | startTransition(() =>
44 | setPokemon(
45 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
46 | )
47 | )
48 | }
49 | >
50 | Next
51 |
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 |
44 | startTransition(() =>
45 | setPokemon(
46 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
47 | )
48 | )
49 | }
50 | >
51 | Next
52 |
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 |
44 | startTransition(() =>
45 | setPokemon(
46 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
47 | )
48 | )
49 | }
50 | >
51 | Next
52 |
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 |
44 | startTransition(() =>
45 | setPokemon(
46 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
47 | )
48 | )
49 | }
50 | >
51 | Next
52 |
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 | onClick(pokemon.url.split("/")[6])}
18 | >
19 | {pokemon.name}
20 |
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 |
51 | startTransition(() =>
52 | setPokemon(
53 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
54 | )
55 | )
56 | }
57 | >
58 | Next
59 |
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 | onClick(pokemon.id)}>
16 | {pokemon.name}
17 |
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 |
48 | startTransition(() =>
49 | setPokemon(
50 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
51 | )
52 | )
53 | }
54 | >
55 | Next
56 |
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 |
46 | startTransition(() =>
47 | setPokemon(
48 | suspensify(fetchPokemon(deferredPokemon.read().id + 1))
49 | )
50 | )
51 | }
52 | >
53 | Next
54 |
55 |
56 |
57 |
58 | Fetching the Database...}>
59 |
60 | (
62 |
63 |
66 | startTransition(() =>
67 | setPokemon(suspensify(fetchPokemon(pokemon.id)))
68 | )
69 | }
70 | >
71 | {pokemon.name}
72 |
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 |
58 | startTransition(() =>
59 | setCollection(
60 | suspensify(
61 | fetchPokemonCollectionUrl(collection.read().next)
62 | )
63 | )
64 | )
65 | }
66 | >
67 | Next
68 |
69 | {isPending && }
70 |
71 |
72 | (
75 |
76 |
80 | startTransition(() =>
81 | setPokemon(suspensify(fetchPokemon(pokemon.id)))
82 | )
83 | }
84 | >
85 | {pokemon.name}
86 |
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 |
50 | startTransition(() =>
51 | setCollectionResource(
52 | suspensify(
53 | fetchPokemonCollectionUrl(
54 | collectionResource.read().next
55 | )
56 | )
57 | )
58 | )
59 | }
60 | >
61 | Next
62 |
63 |
64 | {isPending && }
65 |
*/}
66 |
67 |
68 |
69 | {({ setPokemon }) => (
70 | (
75 |
76 | setPokemon(pokemon.id)}
81 | >
82 |
87 | {pokemon.name}
88 |
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 |
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 |
--------------------------------------------------------------------------------