9 | }
10 | Usage.title = 'Fundamental Suspense'
11 |
12 | export default Usage
13 |
--------------------------------------------------------------------------------
/src/exercises/11.js:
--------------------------------------------------------------------------------
1 | // Where to NOT use hooks
2 | import React from 'react'
3 |
4 | // Don't make changes to the Usage component. It's here to show you how your
5 | // component is intended to be used and is used in the tests.
6 |
7 | function Usage() {
8 | return
When NOT to use hooks (see the final example code)
9 | }
10 | Usage.title = 'Where to NOT use hooks'
11 |
12 | export default Usage
13 |
--------------------------------------------------------------------------------
/src/exercises/14.js:
--------------------------------------------------------------------------------
1 | // Suspense with react-cache (VERY ALPHA)
2 | import React from 'react'
3 |
4 | // Don't make changes to the Usage component. It's here to show you how your
5 | // component is intended to be used and is used in the tests.
6 |
7 | function Usage() {
8 | return
No exercise for this one...
9 | }
10 | Usage.title = 'Suspense with react-cache (VERY ALPHA)'
11 |
12 | export default Usage
13 |
--------------------------------------------------------------------------------
/scripts/make-appveyor-work.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const pkg = require('../package.json')
3 |
4 | // See this for details: https://github.com/aredridel/node-bin-gen/issues/45
5 | delete pkg.devDependencies.node
6 |
7 | // no idea what's going on here...
8 | pkg.scripts.lint = 'echo "disabled for appveyor"'
9 |
10 | fs.writeFileSync(
11 | require.resolve('../package.json'),
12 | JSON.stringify(pkg, null, 2),
13 | )
14 |
--------------------------------------------------------------------------------
/src/exercises-final/01.js:
--------------------------------------------------------------------------------
1 | // Counter: useState
2 | import React, {useState} from 'react'
3 |
4 | function Counter() {
5 | const [count, setCount] = useState(0)
6 | const incrementCount = () => setCount(count + 1)
7 | return
8 | }
9 |
10 | // Don't make changes to the Usage component. It's here to show you how your
11 | // component is intended to be used and is used in the tests.
12 |
13 | function Usage() {
14 | return
15 | }
16 | Usage.title = 'Counter: useState'
17 |
18 | export default Usage
19 |
--------------------------------------------------------------------------------
/src/pokemon-info.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {unstable_createResource as createResource} from 'react-cache'
3 | import fetchPokemon from './fetch-pokemon'
4 |
5 | const myPokemon = createResource(fetchPokemon)
6 |
7 | function FetchPokemon({pokemonName}) {
8 | const pokemon = myPokemon.read(pokemonName)
9 | return
20 | )
21 | }
22 |
23 | export default Tilt
24 |
--------------------------------------------------------------------------------
/src/exercises-final/03.js:
--------------------------------------------------------------------------------
1 | // Counter: useEffect
2 | import React, {useState, useEffect} from 'react'
3 |
4 | function Counter() {
5 | const [count, setCount] = useState(
6 | Number(window.localStorage.getItem('count') || 0),
7 | )
8 | const incrementCount = () => setCount(count + 1)
9 | useEffect(() => {
10 | window.localStorage.setItem('count', count)
11 | })
12 | return
13 | }
14 |
15 | // Don't make changes to the Usage component. It's here to show you how your
16 | // component is intended to be used and is used in the tests.
17 |
18 | function Usage() {
19 | return
20 | }
21 | Usage.title = 'Counter: useEffect'
22 |
23 | export default Usage
24 |
--------------------------------------------------------------------------------
/src/exercises-final/02.js:
--------------------------------------------------------------------------------
1 | // Counter: custom hooks
2 | import React, {useState} from 'react'
3 |
4 | function useCounter(initialCount) {
5 | const [count, setCount] = useState(initialCount)
6 | const incrementCount = () => setCount(count + 1)
7 | return {count, incrementCount}
8 | }
9 |
10 | function Counter() {
11 | const {count, incrementCount} = useCounter(0)
12 | return
13 | }
14 |
15 | // Don't make changes to the Usage component. It's here to show you how your
16 | // component is intended to be used and is used in the tests.
17 |
18 | function Usage() {
19 | return
20 | }
21 | Usage.title = 'Counter: custom hooks'
22 |
23 | export {useCounter}
24 | export default Usage
25 |
--------------------------------------------------------------------------------
/src/exercises-final/04.js:
--------------------------------------------------------------------------------
1 | // Counter: optimizations
2 | import React, {useState, useEffect} from 'react'
3 |
4 | function Counter() {
5 | const [count, setCount] = useState(() =>
6 | Number(window.localStorage.getItem('count') || 0),
7 | )
8 | const incrementCount = () => setCount(count + 1)
9 | useEffect(
10 | () => {
11 | window.localStorage.setItem('count', count)
12 | },
13 | [count],
14 | )
15 | return
16 | }
17 |
18 | // Don't make changes to the Usage component. It's here to show you how your
19 | // component is intended to be used and is used in the tests.
20 |
21 | function Usage() {
22 | return
23 | }
24 | Usage.title = 'Counter: optimizations'
25 |
26 | export default Usage
27 |
--------------------------------------------------------------------------------
/scripts/verify.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | var verifySystem = require('./workshop-setup').verifySystem
4 |
5 | var verifyPromise = verifySystem([
6 | verifySystem.validators.node('>=8.9.4'),
7 | verifySystem.validators.npm('>=5.6.0'),
8 | ])
9 |
10 | verifyPromise.then(
11 | function() {
12 | // resolves if there are no errors
13 | console.log('🎉 Congrats! Your system is setup properly')
14 | console.log('You should be good to install and run things.')
15 | },
16 | function(error) {
17 | // rejects if there are errors
18 | console.error(error)
19 | console.info(
20 | "\nIf you don't care about these warnings, go " +
21 | 'ahead and install dependencies with `node ./scripts/install`',
22 | )
23 | process.exitCode = 1
24 | },
25 | )
26 |
--------------------------------------------------------------------------------
/src/exercises/01.js:
--------------------------------------------------------------------------------
1 | // Counter: useState
2 | // 🐨 you'll need to add {useState} to this import statement
3 | import React from 'react'
4 |
5 | // 💰 the `useState` hook allows you to use state
6 | // from within function components in react:
7 | // const [name, setName] = useState('Angela')
8 |
9 | function Counter() {
10 | // 🐨 you'll call useState here to get count and setCount
11 | // 🐨 render the count here and add an onClick handler that increments the count
12 | return
13 | }
14 |
15 | // Don't make changes to the Usage component. It's here to show you how your
16 | // component is intended to be used and is used in the tests.
17 |
18 | function Usage() {
19 | return
20 | }
21 | Usage.title = 'Counter: useState'
22 |
23 | export default Usage
24 |
--------------------------------------------------------------------------------
/src/exercises/02.js:
--------------------------------------------------------------------------------
1 | // Counter: custom hooks
2 | import React, {useState} from 'react'
3 |
4 | // 🐨 create a function here called useCounter
5 | // the "use" prefix is a convention, and not required.
6 | // don't overthink this. It's JavaScript :)
7 | // 💰 make sure to export it for the tests.
8 |
9 | function Counter() {
10 | // 🐨 move these two lines to your function and return what you need
11 | const [count, setCount] = useState(0)
12 | const incrementCount = () => setCount(count + 1)
13 | return
14 | }
15 |
16 | // Don't make changes to the Usage component. It's here to show you how your
17 | // component is intended to be used and is used in the tests.
18 |
19 | function Usage() {
20 | return
21 | }
22 | Usage.title = 'Counter: custom hooks'
23 |
24 | export default Usage
25 |
--------------------------------------------------------------------------------
/src/fetch-pokemon.js:
--------------------------------------------------------------------------------
1 | function fetchPokemon(name) {
2 | const pokemonQuery = `
3 | query ($name: String) {
4 | pokemon(name: $name) {
5 | id
6 | number
7 | name
8 | attacks {
9 | special {
10 | name
11 | type
12 | damage
13 | }
14 | }
15 | }
16 | }
17 | `
18 | return window
19 | .fetch('https://graphql-pokemon.now.sh', {
20 | // learn more about this API here: https://graphql-pokemon.now.sh/
21 | method: 'POST',
22 | headers: {
23 | 'content-type': 'application/json;charset=UTF-8',
24 | },
25 | body: JSON.stringify({
26 | query: pokemonQuery,
27 | variables: {name},
28 | }),
29 | })
30 | .then(r => r.json())
31 | .then(response => response.data.pokemon)
32 | }
33 |
34 | export default fetchPokemon
35 |
--------------------------------------------------------------------------------
/src/exercises/03.js:
--------------------------------------------------------------------------------
1 | // Counter: useEffect
2 | // 🐨 2. you'll also want useEffect here!
3 | import React, {useState} from 'react'
4 |
5 | // We moved things back to within the Counter component for the exercise.
6 |
7 | function Counter() {
8 | // 🐨 1. initialize the state to the value from localStorage
9 | // 💰 Number(window.localStorage.getItem('count') || 0)
10 | const [count, setCount] = useState(0)
11 | const incrementCount = () => setCount(count + 1)
12 | // 3. 🐨 Here's where you'll use `useEffect`.
13 | // The callback should set the `count` in localStorage.
14 | return
15 | }
16 |
17 | // Don't make changes to the Usage component. It's here to show you how your
18 | // component is intended to be used and is used in the tests.
19 |
20 | function Usage() {
21 | return
22 | }
23 | Usage.title = 'Counter: useEffect'
24 |
25 | export default Usage
26 |
--------------------------------------------------------------------------------
/src/tilt.css:
--------------------------------------------------------------------------------
1 | /*
2 | Taken from the vanilla-tilt.js demo site:
3 | https://micku7zu.github.io/vanilla-tilt.js/index.html
4 | */
5 | .tilt-root {
6 | height: 150px;
7 | background-color: red;
8 | width: 200px;
9 | background-image: -webkit-linear-gradient(315deg, #ff00ba 0%, #fae713 100%);
10 | background-image: linear-gradient(135deg, #ff00ba 0%, #fae713 100%);
11 | transform-style: preserve-3d;
12 | will-change: transform;
13 | transform: perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1);
14 | }
15 | .tilt-child {
16 | position: absolute;
17 | width: 50%;
18 | height: 50%;
19 | top: 50%;
20 | left: 50%;
21 | transform: translateZ(30px) translateX(-50%) translateY(-50%);
22 | box-shadow: 0 0 50px 0 rgba(51, 51, 51, 0.3);
23 | background-color: white;
24 | }
25 | .totally-centered {
26 | width: 100%;
27 | height: 100%;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
--------------------------------------------------------------------------------
/src/__tests__/10.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent} from 'react-testing-library'
3 | import Usage from '../exercises-final/10'
4 | // import Usage from '../exercises/10'
5 |
6 | test('renders upper case first and last name', async () => {
7 | const {getByLabelText, getByText} = render()
8 | fireEvent.change(getByLabelText(/first/i), {target: {value: 'first'}})
9 | getByText(/FIRST/)
10 | fireEvent.change(getByLabelText(/last/i), {target: {value: 'last'}})
11 | getByText(/LAST/)
12 | })
13 |
14 | //////// Elaboration & Feedback /////////
15 | // When you've finished with the exercises:
16 | // 1. Copy the URL below into your browser and fill out the form
17 | // 2. remove the `.skip` from the test below
18 | // 3. Change submitted from `false` to `true`
19 | // 4. And you're all done!
20 | /*
21 | http://ws.kcd.im/?ws=modern%20react&e=10&em=
22 | */
23 | test.skip('I submitted my elaboration and feedback', () => {
24 | const submitted = false // change this when you've submitted!
25 | expect(submitted).toBe(true)
26 | })
27 | ////////////////////////////////
28 |
--------------------------------------------------------------------------------
/src/exercises-final/12.js:
--------------------------------------------------------------------------------
1 | // VanillaTilt: React.lazy
2 | import React, {Suspense, useState} from 'react'
3 |
4 | const Tilt = React.lazy(() => import('../tilt'))
5 |
6 | // Don't make changes to the Usage component. It's here to show you how your
7 | // component is intended to be used and is used in the tests.
8 |
9 | function Usage() {
10 | const [showTilt, setShowTilt] = useState()
11 | return (
12 |
13 |
21 |
22 |
23 | {showTilt ? (
24 |
25 |
26 |
vanilla-tilt.js
27 |
28 |
29 | ) : null}
30 |
31 |
32 |
33 | )
34 | }
35 | Usage.title = 'VanillaTilt: React.lazy'
36 |
37 | export default Usage
38 |
--------------------------------------------------------------------------------
/src/__tests__/01.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent} from 'react-testing-library'
3 | // import Usage from '../exercises-final/01'
4 | import Usage from '../exercises/01'
5 |
6 | test('Usage works', () => {
7 | const {container} = render()
8 | const button = container.getElementsByTagName('button')[0]
9 | expect(button).toHaveTextContent(/0/)
10 | fireEvent.click(button)
11 | expect(button).toHaveTextContent(/1/)
12 | fireEvent.click(button)
13 | expect(button).toHaveTextContent(/2/)
14 | })
15 |
16 | //////// Elaboration & Feedback /////////
17 | // When you've finished with the exercises:
18 | // 1. Copy the URL below into your browser and fill out the form
19 | // 2. remove the `.skip` from the test below
20 | // 3. Change submitted from `false` to `true`
21 | // 4. And you're all done!
22 | /*
23 | http://ws.kcd.im/?ws=modern%20react&e=01&em=
24 | */
25 | test.skip('I submitted my elaboration and feedback', () => {
26 | const submitted = false // change this when you've submitted!
27 | expect(submitted).toBe(true)
28 | })
29 | ////////////////////////////////
30 |
--------------------------------------------------------------------------------
/src/exercises-final/05.js:
--------------------------------------------------------------------------------
1 | // VanillaTilt: useRef
2 | import React, {useRef, useLayoutEffect} from 'react'
3 | import VanillaTilt from 'vanilla-tilt'
4 |
5 | function Tilt(props) {
6 | const tiltNode = useRef()
7 | useLayoutEffect(() => {
8 | const vanillaTiltOptions = {
9 | max: 25,
10 | speed: 400,
11 | glare: true,
12 | 'max-glare': 0.5,
13 | }
14 | VanillaTilt.init(tiltNode.current, vanillaTiltOptions)
15 | return () => tiltNode.current.vanillaTilt.destroy()
16 | }, [])
17 | return (
18 |
19 |
{props.children}
20 |
21 | )
22 | }
23 |
24 | // Don't make changes to the Usage component. It's here to show you how your
25 | // component is intended to be used and is used in the tests.
26 |
27 | function Usage() {
28 | return (
29 |
32 | )
33 | }
34 |
35 | // Don't make changes to the Usage component. It's here to show you how your
36 | // component is intended to be used and is used in the tests.
37 |
38 | function Usage() {
39 | return
40 | }
41 | Usage.title = 'React.memo'
42 |
43 | export default Usage
44 |
--------------------------------------------------------------------------------
/src/__tests__/04.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent, flushEffects} from 'react-testing-library'
3 | import Usage from '../exercises-final/04'
4 | // import Usage from '../exercises/04'
5 |
6 | afterEach(() => {
7 | window.localStorage.removeItem('count')
8 | })
9 |
10 | test('Usage works', () => {
11 | window.localStorage.setItem('count', 3)
12 | const {container} = render()
13 | const button = container.getElementsByTagName('button')[0]
14 | expect(button).toHaveTextContent(/3/)
15 | fireEvent.click(button)
16 | expect(button).toHaveTextContent(/4/)
17 | fireEvent.click(button)
18 | expect(button).toHaveTextContent(/5/)
19 | flushEffects()
20 | expect(window.localStorage.getItem('count')).toBe('5')
21 | })
22 |
23 | //////// Elaboration & Feedback /////////
24 | // When you've finished with the exercises:
25 | // 1. Copy the URL below into your browser and fill out the form
26 | // 2. remove the `.skip` from the test below
27 | // 3. Change submitted from `false` to `true`
28 | // 4. And you're all done!
29 | /*
30 | http://ws.kcd.im/?ws=modern%20react&e=04&em=
31 | */
32 | test.skip('I submitted my elaboration and feedback', () => {
33 | const submitted = false // change this when you've submitted!
34 | expect(submitted).toBe(true)
35 | })
36 | ////////////////////////////////
37 |
--------------------------------------------------------------------------------
/src/__tests__/03.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent, flushEffects} from 'react-testing-library'
3 | import Usage from '../exercises-final/03'
4 | // import Usage from '../exercises/03'
5 |
6 | afterEach(() => {
7 | window.localStorage.removeItem('count')
8 | })
9 |
10 | test('Usage works', async () => {
11 | window.localStorage.setItem('count', 3)
12 | const {container} = render()
13 | const button = container.getElementsByTagName('button')[0]
14 | expect(button).toHaveTextContent(/3/)
15 | fireEvent.click(button)
16 | expect(button).toHaveTextContent(/4/)
17 | fireEvent.click(button)
18 | expect(button).toHaveTextContent(/5/)
19 | flushEffects()
20 | expect(window.localStorage.getItem('count')).toBe('5')
21 | })
22 |
23 | //////// Elaboration & Feedback /////////
24 | // When you've finished with the exercises:
25 | // 1. Copy the URL below into your browser and fill out the form
26 | // 2. remove the `.skip` from the test below
27 | // 3. Change submitted from `false` to `true`
28 | // 4. And you're all done!
29 | /*
30 | http://ws.kcd.im/?ws=modern%20react&e=03&em=
31 | */
32 | test.skip('I submitted my elaboration and feedback', () => {
33 | const submitted = false // change this when you've submitted!
34 | expect(submitted).toBe(true)
35 | })
36 | ////////////////////////////////
37 |
--------------------------------------------------------------------------------
/src/exercises/12.js:
--------------------------------------------------------------------------------
1 | // VanillaTilt: React.lazy
2 | // 🐨 3. add "Suspense" as an import from react here
3 | import React, {useState} from 'react'
4 | // 🐨 1. remove this import
5 | import Tilt from '../tilt'
6 |
7 | // 🐨 2. Use React.lazy with a dynamic import of ../tilt and assign that
8 | // to a variable called Tilt
9 |
10 | function App() {
11 | const [showTilt, setShowTilt] = useState()
12 | return (
13 |
14 |
22 |
23 | {/* 🐨 4. Add a Suspense element here with a fallback="loading..." prop */}
24 | {showTilt ? (
25 |
26 |
27 |
vanilla-tilt.js
28 |
29 |
30 | ) : null}
31 | {/* close Suspense... then checkout the network tab when you check the box! */}
32 |
33 |
34 | )
35 | }
36 |
37 | // Don't make changes to the Usage component. It's here to show you how your
38 | // component is intended to be used and is used in the tests.
39 |
40 | function Usage() {
41 | return
42 | }
43 | Usage.title = 'VanillaTilt: React.lazy'
44 |
45 | export default Usage
46 |
--------------------------------------------------------------------------------
/src/exercises-final/14.js:
--------------------------------------------------------------------------------
1 | // Suspense with react-cache (VERY ALPHA)
2 | import React, {Suspense, useState} from 'react'
3 | import {unstable_createResource as createResource} from 'react-cache'
4 | import fetchPokemon from '../fetch-pokemon'
5 |
6 | const myPokemon = createResource(fetchPokemon)
7 |
8 | function FetchPokemon({pokemonName}) {
9 | const pokemon = myPokemon.read(pokemonName)
10 | return
{JSON.stringify(pokemon || 'Unknown', null, 2)}
11 | }
12 |
13 | function PokemonInfo({pokemonName}) {
14 | return (
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | // Don't make changes to the Usage component. It's here to show you how your
22 | // component is intended to be used and is used in the tests.
23 | function Usage() {
24 | const [pokemonName, setPokemonName] = useState(null)
25 | function handleSubmit(e) {
26 | e.preventDefault()
27 | setPokemonName(e.target.elements.pokemonName.value)
28 | }
29 | return (
30 |
31 |
36 |
37 | {pokemonName ? : null}
38 |
39 |
40 | )
41 | }
42 | Usage.title = 'Suspense with react-cache (VERY ALPHA)'
43 |
44 | export default Usage
45 |
--------------------------------------------------------------------------------
/src/exercises/04.js:
--------------------------------------------------------------------------------
1 | // Counter: optimizations
2 | import React, {useState, useEffect} from 'react'
3 |
4 | function Counter() {
5 | // Right now, we're reading localStorage on every render
6 | // But we only really need to read that value for the first render
7 | // 🐨 1. instead of passing the value to useState as we are now,
8 | // pass a function which returns the value.
9 | const [count, setCount] = useState(
10 | Number(window.localStorage.getItem('count') || 0),
11 | )
12 | const incrementCount = () => setCount(count + 1)
13 | useEffect(
14 | () => {
15 | window.localStorage.setItem('count', count)
16 | },
17 | // 🐨 2. pass a second argument to useEffect right here
18 | // it should be an array of the callback's "dependencies"
19 | // Said differently: "What variables does the useEffect callback use?"
20 | // pass those variables as a second argument as an array.
21 | // React _only_ calls the effect callback:
22 | // 1. After the first render
23 | // 2. After a render during which any element in the dependencies array changes.
24 | // (If there is no array provided, then it is called after every render.)
25 | )
26 | return
27 | }
28 |
29 | // Don't make changes to the Usage component. It's here to show you how your
30 | // component is intended to be used and is used in the tests.
31 |
32 | function Usage() {
33 | return
34 | }
35 | Usage.title = 'Counter: optimizations'
36 |
37 | export default Usage
38 |
--------------------------------------------------------------------------------
/src/__tests__/02.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent} from 'react-testing-library'
3 | import Usage, {useCounter} from '../exercises-final/02'
4 | // import Usage, {useCounter} from '../exercises/02'
5 |
6 | test('Usage works', () => {
7 | const {container} = render()
8 | const button = container.getElementsByTagName('button')[0]
9 | expect(button).toHaveTextContent(/0/)
10 | fireEvent.click(button)
11 | expect(button).toHaveTextContent(/1/)
12 | fireEvent.click(button)
13 | expect(button).toHaveTextContent(/2/)
14 | })
15 |
16 | test('useCounter works', () => {
17 | function Test() {
18 | const {count, incrementCount} = useCounter(2)
19 | return
20 | }
21 | const {container} = render()
22 | const button = container.getElementsByTagName('button')[0]
23 | expect(button).toHaveTextContent(/2/)
24 | fireEvent.click(button)
25 | expect(button).toHaveTextContent(/3/)
26 | fireEvent.click(button)
27 | expect(button).toHaveTextContent(/4/)
28 | })
29 |
30 | //////// Elaboration & Feedback /////////
31 | // When you've finished with the exercises:
32 | // 1. Copy the URL below into your browser and fill out the form
33 | // 2. remove the `.skip` from the test below
34 | // 3. Change submitted from `false` to `true`
35 | // 4. And you're all done!
36 | /*
37 | http://ws.kcd.im/?ws=modern%20react&e=02&em=
38 | */
39 | test.skip('I submitted my elaboration and feedback', () => {
40 | const submitted = false // change this when you've submitted!
41 | expect(submitted).toBe(true)
42 | })
43 | ////////////////////////////////
44 |
--------------------------------------------------------------------------------
/src/exercises-final/13.js:
--------------------------------------------------------------------------------
1 | // Fundamental Suspense
2 | import React, {Suspense, useState} from 'react'
3 | import ErrorBoundary from 'react-error-boundary'
4 | import fetchPokemon from '../fetch-pokemon'
5 |
6 | const cache = {}
7 |
8 | function FetchPokemon({pokemonName}) {
9 | const pokemon = cache[pokemonName]
10 | if (!pokemon) {
11 | const promise = fetchPokemon(pokemonName).then(
12 | pokemon => (cache[pokemonName] = pokemon),
13 | )
14 | throw promise
15 | }
16 | return
{JSON.stringify(pokemon || 'Unknown', null, 2)}
17 | }
18 |
19 | function PokemonInfo({pokemonName}) {
20 | return (
21 | 'There was an error...'}>
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | // Don't make changes to the Usage component. It's here to show you how your
30 | // component is intended to be used and is used in the tests.
31 | function Usage() {
32 | const [pokemonName, setPokemonName] = useState(null)
33 | function handleSubmit(e) {
34 | e.preventDefault()
35 | setPokemonName(e.target.elements.pokemonName.value)
36 | }
37 | return (
38 |
56 | )
57 | }
58 |
59 | // Don't make changes to the Usage component. It's here to show you how your
60 | // component is intended to be used and is used in the tests.
61 |
62 | function Usage() {
63 | return
64 | }
65 | Usage.title = 'Stopwatch: useEffect cleanup'
66 |
67 | export default Usage
68 |
--------------------------------------------------------------------------------
/src/__tests__/13.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent, wait} from 'react-testing-library'
3 | import mockFetchPokemon from '../fetch-pokemon'
4 | import Usage from '../exercises-final/13'
5 | // import Usage from '../exercises/13'
6 |
7 | jest.mock('../fetch-pokemon', () =>
8 | jest.fn(name => Promise.resolve({mokePokemon: true, name})),
9 | )
10 |
11 | test('fetches pokemon data when form is submitted', async () => {
12 | const {getByLabelText, getByText} = render()
13 | const name = getByLabelText(/name/i)
14 | name.value = 'Charzard'
15 | fireEvent.click(getByText(/submit/i))
16 | expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
17 |
18 | mockFetchPokemon.mockClear()
19 |
20 | await wait(() => getByText(/Charzard/))
21 | name.value = 'Pikachu'
22 | fireEvent.click(getByText(/submit/i))
23 | expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
24 | await wait(() => getByText(/Pikachu/))
25 |
26 | mockFetchPokemon.mockClear()
27 |
28 | // TODO: the error case leads to an infinite loop in react
29 | // mockFetchPokemon.mockRejectedValue({error: 'fake failure'})
30 | // name.value = 'fail'
31 | // fireEvent.click(getByText(/submit/i))
32 | // expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
33 | // await wait(() => getByText(/error/i))
34 | })
35 |
36 | //////// Elaboration & Feedback /////////
37 | // When you've finished with the exercises:
38 | // 1. Copy the URL below into your browser and fill out the form
39 | // 2. remove the `.skip` from the test below
40 | // 3. Change submitted from `false` to `true`
41 | // 4. And you're all done!
42 | /*
43 | http://ws.kcd.im/?ws=modern%20react&e=13&em=
44 | */
45 | test.skip('I submitted my elaboration and feedback', () => {
46 | const submitted = false // change this when you've submitted!
47 | expect(submitted).toBe(true)
48 | })
49 | ////////////////////////////////
50 |
--------------------------------------------------------------------------------
/src/__tests__/14.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent, wait} from 'react-testing-library'
3 | import mockFetchPokemon from '../fetch-pokemon'
4 | import Usage from '../exercises-final/14'
5 | // import Usage from '../exercises/14'
6 |
7 | jest.mock('../fetch-pokemon', () =>
8 | jest.fn(name => Promise.resolve({mokePokemon: true, name})),
9 | )
10 |
11 | test('fetches pokemon data when form is submitted', async () => {
12 | const {getByLabelText, getByText} = render()
13 | const name = getByLabelText(/name/i)
14 | name.value = 'Charzard'
15 | fireEvent.click(getByText(/submit/i))
16 | expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
17 |
18 | mockFetchPokemon.mockClear()
19 |
20 | await wait(() => getByText(/Charzard/))
21 | name.value = 'Pikachu'
22 | fireEvent.click(getByText(/submit/i))
23 | expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
24 | await wait(() => getByText(/Pikachu/))
25 |
26 | mockFetchPokemon.mockClear()
27 |
28 | // TODO: the error case leads to an infinite loop in react
29 | // mockFetchPokemon.mockRejectedValue({error: 'fake failure'})
30 | // name.value = 'fail'
31 | // fireEvent.click(getByText(/submit/i))
32 | // expect(mockFetchPokemon).toHaveBeenCalledTimes(1)
33 | // await wait(() => getByText(/error/i))
34 | })
35 |
36 | //////// Elaboration & Feedback /////////
37 | // When you've finished with the exercises:
38 | // 1. Copy the URL below into your browser and fill out the form
39 | // 2. remove the `.skip` from the test below
40 | // 3. Change submitted from `false` to `true`
41 | // 4. And you're all done!
42 | /*
43 | http://ws.kcd.im/?ws=modern%20react&e=14&em=
44 | */
45 | test.skip('I submitted my elaboration and feedback', () => {
46 | const submitted = false // change this when you've submitted!
47 | expect(submitted).toBe(true)
48 | })
49 | ////////////////////////////////
50 |
--------------------------------------------------------------------------------
/src/exercises-final/08.js:
--------------------------------------------------------------------------------
1 | // Stopwatch: useReducer (a la setState)
2 | import React, {useReducer, useEffect, useRef} from 'react'
3 |
4 | const buttonStyles = {
5 | border: '1px solid #ccc',
6 | background: '#fff',
7 | fontSize: '2em',
8 | padding: 15,
9 | margin: 5,
10 | width: 200,
11 | }
12 |
13 | function reducer(currentState, newState) {
14 | return {...currentState, ...newState}
15 | }
16 |
17 | function Stopwatch() {
18 | const [{running, lapse}, setState] = useReducer(reducer, {
19 | running: false,
20 | lapse: 0,
21 | })
22 | const timerRef = useRef(null)
23 |
24 | useEffect(() => () => clearInterval(timerRef.current), [])
25 |
26 | function handleRunClick() {
27 | if (running) {
28 | clearInterval(timerRef.current)
29 | } else {
30 | const startTime = Date.now() - lapse
31 | timerRef.current = setInterval(() => {
32 | setState({lapse: Date.now() - startTime})
33 | }, 0)
34 | }
35 | setState({running: !running})
36 | }
37 |
38 | function handleClearClick() {
39 | clearInterval(timerRef.current)
40 | setState({running: false, lapse: 0})
41 | }
42 |
43 | return (
44 |
45 |
54 |
57 |
60 |
61 | )
62 | }
63 |
64 | // Don't make changes to the Usage component. It's here to show you how your
65 | // component is intended to be used and is used in the tests.
66 |
67 | function Usage() {
68 | return
69 | }
70 | Usage.title = 'Stopwatch: useReducer (a la setState)'
71 |
72 | export default Usage
73 |
--------------------------------------------------------------------------------
/scripts/autofill-feedback-email.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | const path = require('path')
3 | const inquirer = require('inquirer')
4 | const replace = require('replace-in-file')
5 | const isCI = require('is-ci')
6 | const spawn = require('cross-spawn')
7 | const {EOL} = require('os')
8 |
9 | if (isCI) {
10 | console.log(`Not running autofill feedback as we're on CI`)
11 | } else {
12 | const prompt = inquirer.prompt([
13 | {
14 | name: 'email',
15 | message: `what's your email address?`,
16 | validate(val) {
17 | if (!val) {
18 | // they don't want to do this...
19 | return true
20 | } else if (!val.includes('@')) {
21 | return 'email requires an @ sign...'
22 | }
23 | return true
24 | },
25 | },
26 | ])
27 | const timeoutId = setTimeout(() => {
28 | console.log(
29 | `\n\nprompt timed out. No worries. Run \`node ./scripts/autofill-feedback-email.js\` if you'd like to try again`,
30 | )
31 | prompt.ui.close()
32 | }, 15000)
33 |
34 | prompt.then(({email} = {}) => {
35 | clearTimeout(timeoutId)
36 | if (!email) {
37 | console.log(`Not autofilling email because none was provided`)
38 | return
39 | }
40 | const options = {
41 | files: [path.join(__dirname, '..', 'src/**/*.js')],
42 | from: `&em=${EOL}`,
43 | to: `&em=${email}${EOL}`,
44 | }
45 |
46 | replace(options).then(
47 | changedFiles => {
48 | console.log(`Updated ${changedFiles.length} with the email ${email}`)
49 | console.log(
50 | 'committing changes for you so your jest watch mode works nicely',
51 | )
52 | spawn.sync('git', ['commit', '-am', 'email autofill', '--no-verify'], {
53 | stdio: 'inherit',
54 | })
55 | },
56 | error => {
57 | console.error('Failed to update files')
58 | console.error(error.stack)
59 | },
60 | )
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/src/exercises/05.js:
--------------------------------------------------------------------------------
1 | // VanillaTilt: useRef
2 | // 🐨 1. you'll want useRef, and useLayoutEffect
3 | // you can use `useLayoutEffect` the same way you use `useEffect`.
4 | // Make sure to ask me what the difference is!
5 | // https://reactjs.org/docs/hooks-reference.html#useref
6 | // https://reactjs.org/docs/hooks-reference.html#uselayouteffect
7 | import React from 'react'
8 | // 🐨 2. you'll need this:
9 | // import VanillaTilt from 'vanilla-tilt'
10 |
11 | function Tilt(props) {
12 | // 🐨 3. create a `tiltNode` variable here with `useRef()`
13 | // 🐨 5. create a `useLayoutEffect` callback here which
14 | // uses the `VanillaTilt.init` with `tiltNode.current`
15 | // 🐨 6: you'll need this in your callback:
16 | // const vanillaTiltOptions = {
17 | // max: 25,
18 | // speed: 400,
19 | // glare: true,
20 | // 'max-glare': 0.5,
21 | // }
22 | // 🐨 7. return a cleanup function which will call
23 | // `tiltNode.current.vanillaTilt.destroy()`
24 |
25 | // By default, effects run after every render. This is normally what
26 | // you want, but if you want you can optimize things by ensuring they
27 | // are only called when you specifically want to.
28 | // 💯 add a second argument to your `useLayoutEffect` which lists an empty
29 | // array. We can do this because we only want it to run on initial render
30 | // and we know that the `tiltRef.current` will never change.
31 |
32 | // 🐨 4. pass the `tiltNode` variable to this `div` as the `ref` prop:
33 | return (
34 |
35 |
{props.children}
36 |
37 | )
38 | }
39 |
40 | // Don't make changes to the Usage component. It's here to show you how your
41 | // component is intended to be used and is used in the tests.
42 |
43 | function Usage() {
44 | return (
45 |
46 |
47 |
vanilla-tilt.js
48 |
49 |
50 | )
51 | }
52 | Usage.title = 'VanillaTilt: useRef'
53 |
54 | export default Usage
55 |
--------------------------------------------------------------------------------
/src/__tests__/09.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chalk from 'chalk'
3 | import {render, fireEvent} from 'react-testing-library'
4 | import Usage from '../exercises-final/09'
5 | // import Usage from '../exercises/09'
6 |
7 | const sleep = time => new Promise(resolve => setTimeout(resolve, time))
8 |
9 | test('renders', async () => {
10 | jest.spyOn(console, 'error')
11 | const {container, getAllByText, getByTestId} = render()
12 | const diff = getByTestId('diff')
13 | const timer1 = {
14 | startStop: getAllByText('Start')[0],
15 | clear: getAllByText('Clear')[0],
16 | label: container.querySelectorAll('label')[0],
17 | }
18 | const timer2 = {
19 | startStop: getAllByText('Start')[1],
20 | clear: getAllByText('Clear')[1],
21 | label: container.querySelectorAll('label')[1],
22 | }
23 |
24 | fireEvent.click(timer1.startStop)
25 |
26 | await sleep(200)
27 |
28 | expect(parseInt(diff.textContent, 10)).toBeGreaterThan(150)
29 |
30 | fireEvent.click(timer2.startStop)
31 |
32 | await sleep(200)
33 | try {
34 | expect(parseInt(timer2.label.textContent, 10)).toBeGreaterThan(150)
35 | expect(parseInt(diff.textContent, 10)).toBeLessThan(300)
36 | } catch (error) {
37 | error.message = [
38 | chalk.red(
39 | `🚨 The stopwatch time is not being incremented or we can't find it. Make sure the time lapsed is in a