├── .npmrc ├── .gitignore ├── .DS_Store ├── .prettierignore ├── public ├── antic-slab.woff2 └── index.html ├── src ├── setupTests.js ├── index.js ├── exercises │ ├── 13.js │ ├── 11.js │ ├── 14.js │ ├── 01.js │ ├── 02.js │ ├── 03.js │ ├── 10.js │ ├── 12.js │ ├── 04.js │ ├── 05.js │ ├── 06.js │ ├── 09.js │ ├── 07.js │ └── 08.js ├── exercises-final │ ├── 01.js │ ├── 03.js │ ├── 02.js │ ├── 04.js │ ├── 12.js │ ├── 05.js │ ├── 10.js │ ├── 14.js │ ├── 13.js │ ├── 06.js │ ├── 08.js │ ├── 11.js │ ├── 07.js │ └── 09.js ├── pokemon-info.js ├── tilt.js ├── fetch-pokemon.js ├── tilt.css ├── __tests__ │ ├── 10.js │ ├── 01.js │ ├── 05.js │ ├── 12.js │ ├── 04.js │ ├── 03.js │ ├── 02.js │ ├── 13.js │ ├── 14.js │ ├── 09.js │ ├── 06.js │ ├── 07.js │ └── 08.js └── app.js ├── sandbox.config.json ├── .prettierrc ├── .travis.yml ├── appveyor.yml ├── scripts ├── install.js ├── make-appveyor-work.js ├── verify.js └── autofill-feedback-email.js ├── .all-contributorsrc ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/modern-react/HEAD/.DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | .idea/ 5 | scripts/workshop-setup.js 6 | -------------------------------------------------------------------------------- /public/antic-slab.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/modern-react/HEAD/public/antic-slab.woff2 -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import 'jest-dom/extend-expect' 2 | import 'react-testing-library/cleanup-after-each' 3 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "template": "node" 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './tilt.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import {App} from './app' 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('⚛️')) 7 | root.render() 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": false, 5 | "printWidth": 80, 6 | "proseWrap": "always", 7 | "semi": false, 8 | "singleQuote": true, 9 | "tabWidth": 2, 10 | "trailingComma": "all", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 8 4 | install: echo "Installation happens in the setup script" 5 | cache: 6 | directories: 7 | - node_modules 8 | notifications: 9 | email: false 10 | branches: 11 | only: 12 | - master 13 | script: 14 | - npm run setup 15 | after_success: 16 | - npx codecov 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - node_version: "8" 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | install: 10 | - ps: Install-Product node $env:node_version 11 | 12 | test_script: 13 | - node ./scripts/make-appveyor-work.js 14 | - npm run setup 15 | 16 | cache: 17 | - ./node_modules -> package.json 18 | 19 | build: off 20 | -------------------------------------------------------------------------------- /scripts/install.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path') 3 | const installDeps = require('./workshop-setup').installDeps 4 | 5 | installDeps([path.resolve(__dirname, '..')]).then( 6 | () => { 7 | console.log('👍 all dependencies installed') 8 | }, 9 | () => { 10 | // ignore, workshop-setup will log for us... 11 | }, 12 | ) 13 | -------------------------------------------------------------------------------- /src/exercises/13.js: -------------------------------------------------------------------------------- 1 | // Fundamental Suspense 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 = '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
{JSON.stringify(pokemon || 'Unknown', null, 2)}
10 | } 11 | 12 | function PokemonInfo({pokemonName}) { 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default PokemonInfo 21 | -------------------------------------------------------------------------------- /src/tilt.js: -------------------------------------------------------------------------------- 1 | import React, {useRef, useLayoutEffect} from 'react' 2 | import VanillaTilt from 'vanilla-tilt' 3 | 4 | function Tilt(props) { 5 | const tiltNode = useRef() 6 | useLayoutEffect(() => { 7 | const vanillaTiltOptions = { 8 | max: 25, 9 | speed: 400, 10 | glare: true, 11 | 'max-glare': 0.5, 12 | } 13 | VanillaTilt.init(tiltNode.current, vanillaTiltOptions) 14 | return () => tiltNode.current.vanillaTilt.destroy() 15 | }, []) 16 | return ( 17 |
18 |
{props.children}
19 |
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 |
30 | 31 |
vanilla-tilt.js
32 |
33 |
34 | ) 35 | } 36 | Usage.title = 'VanillaTilt: useRef' 37 | 38 | export default Usage 39 | -------------------------------------------------------------------------------- /src/__tests__/05.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VanillaTilt from 'vanilla-tilt' 3 | import {render} from 'react-testing-library' 4 | import Usage from '../exercises-final/05' 5 | // import Usage from '../exercises/05' 6 | 7 | beforeEach(() => { 8 | jest.spyOn(VanillaTilt, 'init') 9 | }) 10 | 11 | afterEach(() => { 12 | VanillaTilt.init.mockRestore() 13 | }) 14 | 15 | test('calls VanillaTilt.init with the root node', () => { 16 | const {container} = render() 17 | expect(container.querySelector('.tilt-root')).toHaveProperty('vanillaTilt') 18 | expect(VanillaTilt.init).toHaveBeenCalledTimes(1) 19 | }) 20 | 21 | //////// Elaboration & Feedback ///////// 22 | // When you've finished with the exercises: 23 | // 1. Copy the URL below into your browser and fill out the form 24 | // 2. remove the `.skip` from the test below 25 | // 3. Change submitted from `false` to `true` 26 | // 4. And you're all done! 27 | /* 28 | http://ws.kcd.im/?ws=learn%20react&e=05&em= 29 | */ 30 | test.skip('I submitted my elaboration and feedback', () => { 31 | const submitted = false // change this when you've submitted! 32 | expect(submitted).toBe(true) 33 | }) 34 | //////////////////////////////// 35 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "modern-react", 3 | "projectOwner": "kentcdodds", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "contributors": [ 11 | { 12 | "login": "kentcdodds", 13 | "name": "Kent C. Dodds", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 15 | "profile": "https://kentcdodds.com", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "infra", 20 | "test" 21 | ] 22 | }, 23 | { 24 | "login": "acdlite", 25 | "name": "Andrew Clark", 26 | "avatar_url": "https://avatars0.githubusercontent.com/u/3624098?v=4", 27 | "profile": "https://github.com/acdlite", 28 | "contributions": [ 29 | "question", 30 | "ideas", 31 | "review" 32 | ] 33 | }, 34 | { 35 | "login": "sophiebits", 36 | "name": "Sophie Alpert", 37 | "avatar_url": "https://avatars2.githubusercontent.com/u/6820?v=4", 38 | "profile": "https://sophiebits.com/", 39 | "contributions": [ 40 | "question", 41 | "ideas", 42 | "review" 43 | ] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/exercises-final/10.js: -------------------------------------------------------------------------------- 1 | // React.memo 2 | import React, {useState} from 'react' 3 | 4 | const Upper = React.memo(function Upper({children}) { 5 | const [count, setCount] = useState(0) 6 | return ( 7 |
8 | Uppercase version: {children.toUpperCase()}{' '} 9 | 10 |
11 | ) 12 | }) 13 | 14 | function App() { 15 | const [first, setFirstName] = useState('') 16 | const [last, setLastName] = useState('') 17 | return ( 18 |
19 | 20 | setFirstName(e.target.value)} 23 | /> 24 | {first} 25 |
26 | 27 | setLastName(e.target.value)} /> 28 | {last} 29 |
30 | ) 31 | } 32 | 33 | // Don't make changes to the Usage component. It's here to show you how your 34 | // component is intended to be used and is used in the tests. 35 | 36 | function Usage() { 37 | return 38 | } 39 | Usage.title = 'React.memo' 40 | 41 | export default Usage 42 | -------------------------------------------------------------------------------- /src/__tests__/12.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VanillaTilt from 'vanilla-tilt' 3 | import {render, wait, fireEvent} from 'react-testing-library' 4 | import Usage from '../exercises-final/12' 5 | // import Usage from '../exercises/12' 6 | 7 | beforeEach(() => { 8 | jest.spyOn(VanillaTilt, 'init') 9 | }) 10 | 11 | afterEach(() => { 12 | VanillaTilt.init.mockRestore() 13 | }) 14 | 15 | test('calls VanillaTilt.init with the root node', async () => { 16 | const {container, getByText} = render() 17 | fireEvent.click(getByText(/show/i)) 18 | await wait(() => expect(VanillaTilt.init).toHaveBeenCalledTimes(1)) 19 | expect(container.querySelector('.tilt-root')).toHaveProperty('vanillaTilt') 20 | }) 21 | 22 | //////// Elaboration & Feedback ///////// 23 | // When you've finished with the exercises: 24 | // 1. Copy the URL below into your browser and fill out the form 25 | // 2. remove the `.skip` from the test below 26 | // 3. Change submitted from `false` to `true` 27 | // 4. And you're all done! 28 | /* 29 | http://ws.kcd.im/?ws=learn%20react&e=12&em= 30 | */ 31 | test.skip('I submitted my elaboration and feedback', () => { 32 | const submitted = false // change this when you've submitted! 33 | expect(submitted).toBe(true) 34 | }) 35 | //////////////////////////////// 36 | -------------------------------------------------------------------------------- /src/exercises/10.js: -------------------------------------------------------------------------------- 1 | // React.memo 2 | import React, {useState} from 'react' 3 | 4 | // 🐨 1. wrap this in a call to React.memo 5 | // 💰 const MyComponent = React.memo(function MyComponent() {}) 6 | function Upper({children}) { 7 | const [count, setCount] = useState(0) 8 | return ( 9 |
10 | Uppercase version: {children.toUpperCase()}{' '} 11 | 12 |
13 | ) 14 | } 15 | 16 | function App() { 17 | const [first, setFirstName] = useState('') 18 | const [last, setLastName] = useState('') 19 | return ( 20 |
21 | 22 | setFirstName(e.target.value)} 25 | /> 26 | {first} 27 |
28 | 29 | setLastName(e.target.value)} /> 30 | {last} 31 |
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 |
32 | 33 | 34 | 35 |
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 |
39 |
40 | 41 | 42 | 43 |
44 |
45 | {pokemonName ? : null} 46 |
47 |
48 | ) 49 | } 50 | Usage.title = 'Fundamental Suspense' 51 | 52 | export default Usage 53 | -------------------------------------------------------------------------------- /src/exercises-final/06.js: -------------------------------------------------------------------------------- 1 | // Stopwatch: useEffect cleanup 2 | import React, {useState, 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 Stopwatch() { 14 | const [lapse, setLapse] = useState(0) 15 | const [running, setRunning] = useState(false) 16 | const timerRef = useRef(null) 17 | 18 | useEffect(() => () => clearInterval(timerRef.current), []) 19 | 20 | function handleRunClick() { 21 | if (running) { 22 | clearInterval(timerRef.current) 23 | } else { 24 | const startTime = Date.now() - lapse 25 | timerRef.current = setInterval(() => { 26 | setLapse(Date.now() - startTime) 27 | }, 0) 28 | } 29 | setRunning(!running) 30 | } 31 | 32 | function handleClearClick() { 33 | clearInterval(timerRef.current) 34 | setLapse(0) 35 | setRunning(false) 36 | } 37 | 38 | return ( 39 |
40 | 49 | 52 | 55 |
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