/src/exercises/05.md

https://github.com/kentcdodds/concurrent-react · Markdown · 114 lines · 88 code · 26 blank · 0 comment · 0 complexity · 9aa0dd1de4658daeab0fbda086c54500 MD5 · raw file

  1. # Suspense Image
  2. ## Background
  3. Loading images is tricky business because you're handing the asynchronous state
  4. over to the browser. It manages the loading, error, and success states for you.
  5. But what if you have an experience that doesn't look any good until the image is
  6. actually loaded? Or what if you want to render a fallback in the image's place
  7. while it's loading (you want to provide your own loading UI)? In that case,
  8. you're kinda out of luck, because the browser gives us no such API.
  9. Suspense can help us with this too! Luckily for us, we can pre-load images into
  10. the browser's cache using the following code:
  11. ```javascript
  12. function preloadImage(src) {
  13. return new Promise(resolve => {
  14. const img = document.createElement('img')
  15. img.src = src
  16. img.onload = () => resolve(src)
  17. })
  18. }
  19. ```
  20. That function will resolve to the source you gave it as soon as the image has
  21. loaded. Once that promise resolves, you know that the browser has it in it's
  22. cache and any `<img />` elements you render with the `src` set to that `src`
  23. value will get instantly rendered with the image straight from the browser
  24. cache.
  25. ## Exercise
  26. If you turn up the throttle on your network tab (to "Slow 3G" for example) and
  27. select pokemon, you may notice that images take a moment to load in.
  28. For the first one, there's nothing there and then it bumps the content down when
  29. it loads. This can be "fixed" by setting a fixed height for the images. But
  30. let's assume that you can't be sure what that height is.
  31. If you select another pokemon, then that pokemon's data pops in, but the old
  32. pokemon's image remains in place until the new one's image finishes loading.
  33. With suspense, we have an opportunity to make this experience a lot better. We
  34. have two related options:
  35. 1. Make an `Img` component that suspends until the browser has actually loaded
  36. the image.
  37. 2. Make a request for the image alongside the pokemon data.
  38. Option 1 means that nothing will render until both the data and the image are
  39. ready.
  40. Option 2 is even better because it loads the data and image at the same time. It
  41. works because all the images are available via the same information we use to
  42. get the pokemon data.
  43. We're going to do both of these approaches for this exercise (option 2 is extra
  44. credit).
  45. ## Extra Credit
  46. ### 💯 avoid waterfall
  47. If you open up the network tab, you'll notice that you have to load the data
  48. before you can load the image because the data is where we get the image URL.
  49. You may also notice that the image URL is always very predictable. In fact, I
  50. even wrote a function for you to get the image URL based on the pokemon name!
  51. It's exported by `src/fetch-pokemon.js` and is called `getImageUrlForPokemon`.
  52. ```javascript
  53. const imageUrl = getImageUrlForPokemon('pikachu')
  54. ```
  55. Try to pre-load this at the same time as the rest of your data. This one will be
  56. a bit trickier. I'll give you a hint. There are several ways you could do this,
  57. but in my solution, I end up changing the `PokemonInfo` component to this:
  58. ```javascript
  59. function PokemonInfo({pokemonResource}) {
  60. const pokemon = pokemonResource.data.read()
  61. return (
  62. <div>
  63. <div className="pokemon-info__img-wrapper">
  64. <img src={pokemonResource.image.read()} alt={pokemon.name} />
  65. </div>
  66. <PokemonDataView pokemon={pokemon} />
  67. </div>
  68. )
  69. }
  70. ```
  71. ### 💯 Render as you Fetch
  72. Remember when we did this with 02? Now that we're pre-loading the image along
  73. with the data, the improvements will be even more pronounced. Go ahead and put
  74. this at the top of your file:
  75. ```javascript
  76. import createPokemonInfoResource from '../lazy/pokemon-info-render-as-you-fetch-04.data'
  77. const PokemonInfo = React.lazy(() =>
  78. import('../lazy/pokemon-info-render-as-you-fetch-04'),
  79. )
  80. ```
  81. And make that work. Then checkout the network tab and see how your waterfall has
  82. turned into a... stone wall? Yeah!
  83. Again this is a good one to have `window.fetch.restoreOriginalFetch()` called so
  84. you can see a real network request for the data. You will notice that we're
  85. making two requests to the pokemon endpoint. The first is an OPTIONS request.
  86. That's basically a request the browser makes to ask the server if it will accept
  87. our request across domains. It's kind of annoying because it makes our network
  88. requests take longer, but it is what it is.