├── .npmrc ├── epicshop ├── .npmrc ├── in-browser-tests.spec.js ├── update-deps.sh ├── package.json ├── Dockerfile ├── setup-custom.js ├── playwright.config.js ├── fly.yaml └── setup.js ├── public ├── favicon.ico ├── og │ ├── background.png │ └── logo.svg ├── react.js ├── images │ └── instructor.png ├── react-dom │ └── client.js ├── favicon.svg └── logo.svg ├── .prettierignore ├── exercises ├── FINISHED.mdx ├── 03.using-jsx │ ├── 04.solution.nesting │ │ ├── README.mdx │ │ ├── index.html │ │ └── content.test.ts │ ├── 05.solution.fragments │ │ ├── README.mdx │ │ ├── index.html │ │ ├── index.test.ts │ │ └── content.test.ts │ ├── FINISHED.mdx │ ├── 02.solution.interpolation │ │ ├── README.mdx │ │ ├── content.test.ts │ │ ├── index.html │ │ └── index.test.ts │ ├── 03.solution.spread │ │ ├── README.mdx │ │ ├── index.test.ts │ │ ├── content.test.ts │ │ └── index.html │ ├── 04.problem.nesting │ │ ├── README.mdx │ │ └── index.html │ ├── 01.solution.compiler │ │ ├── index.html │ │ ├── content.test.ts │ │ ├── index.test.ts │ │ └── README.mdx │ ├── 01.problem.compiler │ │ ├── README.mdx │ │ └── index.html │ ├── 02.problem.interpolation │ │ ├── index.html │ │ └── README.mdx │ ├── 03.problem.spread │ │ ├── index.html │ │ └── README.mdx │ ├── 05.problem.fragments │ │ ├── index.html │ │ └── README.mdx │ └── README.mdx ├── 02.raw-react │ ├── 03.solution.deep-nesting │ │ ├── README.mdx │ │ ├── index.html │ │ └── index.test.ts │ ├── FINISHED.mdx │ ├── 02.solution.nesting │ │ ├── README.mdx │ │ ├── index.html │ │ └── index.test.ts │ ├── 01.solution.elements │ │ ├── README.mdx │ │ ├── index.html │ │ └── index.test.ts │ ├── 03.problem.deep-nesting │ │ ├── README.mdx │ │ └── index.html │ ├── 02.problem.nesting │ │ ├── README.mdx │ │ └── index.html │ ├── 01.problem.elements │ │ ├── index.html │ │ └── README.mdx │ └── README.mdx ├── 04.components │ ├── 04.solution.props │ │ ├── README.mdx │ │ ├── content.test.ts │ │ └── index.html │ ├── FINISHED.mdx │ ├── 02.solution.raw │ │ ├── README.mdx │ │ ├── index.test.ts │ │ ├── content.test.ts │ │ └── index.html │ ├── 01.solution.function │ │ ├── README.mdx │ │ ├── index.test.ts │ │ ├── index.html │ │ └── content.test.ts │ ├── 03.solution.jsx │ │ ├── index.test.ts │ │ ├── index.html │ │ ├── content.test.ts │ │ └── README.mdx │ ├── 04.problem.props │ │ ├── README.mdx │ │ └── index.html │ ├── 01.problem.function │ │ ├── index.html │ │ └── README.mdx │ ├── 03.problem.jsx │ │ ├── index.html │ │ └── README.mdx │ ├── 02.problem.raw │ │ ├── index.html │ │ └── README.mdx │ └── README.mdx ├── 01.js-hello-world │ ├── FINISHED.mdx │ ├── 01.solution.hello │ │ ├── README.mdx │ │ ├── index.html │ │ └── index.test.ts │ ├── 02.problem.root │ │ ├── README.mdx │ │ └── index.html │ ├── 02.solution.root │ │ ├── README.mdx │ │ ├── index.html │ │ └── index.test.ts │ ├── 01.problem.hello │ │ ├── README.mdx │ │ └── index.html │ └── README.mdx └── README.mdx ├── LICENSE.md ├── tsconfig.json ├── .gitignore ├── eslint.config.js ├── .github └── workflows │ └── validate.yml ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /epicshop/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/get-started-with-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/get-started-with-react/HEAD/public/og/background.png -------------------------------------------------------------------------------- /public/react.js: -------------------------------------------------------------------------------- 1 | export * from 'https://esm.sh/react?dev' 2 | export { default } from 'https://esm.sh/react?dev' 3 | -------------------------------------------------------------------------------- /public/images/instructor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/get-started-with-react/HEAD/public/images/instructor.png -------------------------------------------------------------------------------- /public/react-dom/client.js: -------------------------------------------------------------------------------- 1 | export * from 'https://esm.sh/react-dom/client?dev' 2 | export { default } from 'https://esm.sh/react-dom/client?dev' 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/build/** 3 | **/public/build/** 4 | **/public/babel-standalone.js 5 | .env 6 | **/package-lock.json 7 | **/playwright-report/** -------------------------------------------------------------------------------- /exercises/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Get Started with React 🏃‍➡️ 2 | 3 | 4 | 5 | Hooray! You're all done! 👏👏 6 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/04.solution.nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Nesting JSX 2 | 3 | 4 | 5 | 👨‍💼 Great work! Isn't that so much better than raw `createElement` calls? 😆 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact us 4 | at team@epicweb.dev 5 | -------------------------------------------------------------------------------- /exercises/02.raw-react/03.solution.deep-nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Deep Nesting Elements 2 | 3 | 4 | 5 | 👨‍💼 Great job! We can go as deep as we want with this stuff! 6 | -------------------------------------------------------------------------------- /exercises/02.raw-react/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Raw React APIs 2 | 3 | 4 | 5 | 👨‍💼 Wahoo! I'll bet you're excited to get into JSX after working with the raw 6 | APIs huh? 😅 7 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.solution.fragments/README.mdx: -------------------------------------------------------------------------------- 1 | # Fragments 2 | 3 | 4 | 5 | 👨‍💼 Great! Now you can put elements side-by-side without worrying about their 6 | parent element. 7 | -------------------------------------------------------------------------------- /exercises/04.components/04.solution.props/README.mdx: -------------------------------------------------------------------------------- 1 | # Props 2 | 3 | 4 | 5 | 👨‍💼 Great work! Now you have a solid understanding of how React treats JSX and 6 | custom components! Well done. 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "extends": ["@epic-web/config/typescript"], 4 | "compilerOptions": { 5 | // keep things easy for the exercises 6 | "noUncheckedIndexedAccess": false, 7 | "paths": { 8 | "#*": ["./*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | workspace/ 4 | **/.cache/ 5 | **/build/ 6 | **/public/build 7 | /playground 8 | **/tsconfig.tsbuildinfo 9 | 10 | # in a real app you'd want to not commit the .env 11 | # file as well, but since this is for a workshop 12 | # we're going to keep them around. 13 | # .env 14 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Hello World in JS 2 | 3 | 4 | 5 | 👨‍💼 Great job! You did it! Now's your opportunity to review what you learned. 6 | Writing down what you've learned helps you to remember it better. 7 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.solution.nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Nesting Elements 2 | 3 | 4 | 5 | 👨‍💼 Great! Now we've got a nice way to nest our React elements so we can build 6 | complex structures in our generated HTML. Let's go **even deeper**! 7 | -------------------------------------------------------------------------------- /epicshop/in-browser-tests.spec.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { setupInBrowserTests } from '@epic-web/workshop-utils/playwright.server' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | process.env.EPICSHOP_CONTEXT_CWD = resolve(__dirname, '..') 7 | 8 | setupInBrowserTests() 9 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Using JSX 2 | 3 | 4 | 5 | 👨‍💼 Hooray! You now know how to use JSX effectively to make really nicely 6 | composable user interfaces with React. You've got the fundamentals down, from 7 | here it's just a matter of practice. Great job! 8 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.solution.hello/README.mdx: -------------------------------------------------------------------------------- 1 | # Hello World in JS 2 | 3 | 4 | 5 | 👨‍💼 Awesome job. Now you know how to create DOM nodes using the regular JS APIs. 6 | 7 | But you know what, we could probably do even more in JS... Let's look at that 8 | next. 9 | -------------------------------------------------------------------------------- /exercises/02.raw-react/01.solution.elements/README.mdx: -------------------------------------------------------------------------------- 1 | # Create React Elements 2 | 3 | 4 | 5 | 👨‍💼 Great work! We're well on our way to using React for building UIs on the web! 6 | But most apps are a little more complicated than a single element. Let's go 7 | deeper! 8 | -------------------------------------------------------------------------------- /epicshop/update-deps.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | npx npm-check-updates --dep prod,dev --upgrade --root 4 | cd epicshop && npx npm-check-updates --dep prod,dev --upgrade --root 5 | cd .. 6 | rm -rf node_modules package-lock.json ./epicshop/package-lock.json ./epicshop/node_modules ./exercises/**/node_modules 7 | npm install 8 | npm run setup 9 | npm run typecheck 10 | npm run lint -- --fix 11 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.solution.interpolation/README.mdx: -------------------------------------------------------------------------------- 1 | # Interpolation 2 | 3 | 4 | 5 | 👨‍💼 Great job! Remember, if you can compile the JSX you see into JavaScript 6 | expressions (and vice-versa), you'll be much more efficient with this syntax. 7 | It's all just JavaScript expressions! 8 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import defaultConfig from '@epic-web/config/eslint' 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default [ 5 | { ignores: ['**/babel-standalone.js'] }, 6 | ...defaultConfig, 7 | { 8 | rules: { 9 | // we leave unused vars around for the exercises 10 | 'no-unused-vars': 'off', 11 | '@typescript-eslint/no-unused-vars': 'off', 12 | }, 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.solution.spread/README.mdx: -------------------------------------------------------------------------------- 1 | # Spread props 2 | 3 | 4 | 5 | 👨‍💼 Great! We do this all the time in React. You'll often find yourself wrapping 6 | components to compose things in interesting ways and there's where this pattern 7 | is most useful. We'll get to custom components like that later. 8 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.solution.hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /epicshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "test:setup": "playwright install chromium --with-deps", 5 | "test": "playwright test" 6 | }, 7 | "dependencies": { 8 | "@epic-web/config": "^1.21.0", 9 | "@epic-web/workshop-app": "^6.47.6", 10 | "@epic-web/workshop-utils": "^6.47.6", 11 | "epicshop": "^6.47.6", 12 | "execa": "^9.6.0", 13 | "fs-extra": "^11.3.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/02.problem.root/README.mdx: -------------------------------------------------------------------------------- 1 | # Generate the Root Node 2 | 3 | 4 | 5 | Rather than having the `root` node in the HTML, see if you can create that one 6 | using JavaScript as well. Remove the `
` from the HTML and 7 | instead of trying to find it with `document.getElementById('root')`, create it 8 | and append it to the `document.body`. 9 | -------------------------------------------------------------------------------- /exercises/04.components/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Custom Components 2 | 3 | 4 | 5 | 👨‍💼 Great job! Now you've got a good handle on what it takes to create a custom 6 | component with React. And it's not a surface-level understanding either. You 7 | understand how it works under the hood which will be critical to your experience 8 | building custom components going forward. Great job! 9 | -------------------------------------------------------------------------------- /exercises/02.raw-react/03.problem.deep-nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Deep Nesting Elements 2 | 3 | 4 | 5 | 👨‍💼 Just to make sure we really understand how to nest this stuff. Try to create 6 | this using React's APIs: 7 | 8 | ```html 9 |
10 |

Here's Sam's favorite food:

11 |
    12 |
  • Green eggs
  • 13 |
  • Ham
  • 14 |
15 |
16 | ``` 17 | -------------------------------------------------------------------------------- /exercises/04.components/02.solution.raw/README.mdx: -------------------------------------------------------------------------------- 1 | # Raw API 2 | 3 | 4 | 5 | 👨‍💼 Great! While that's not really all that much nicer to work with, it does get 6 | us a centimeter closer to our goal of using JSX with custom components. It's 7 | also interesting to throw a couple console logs around and see how using 8 | `createElement` compares to simply calling the function directly in our 9 | JSX. 10 | 11 | Now we're finally ready to use JSX for our custom components! Let's go! 12 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/02.solution.root/README.mdx: -------------------------------------------------------------------------------- 1 | # Generate the Root Node 2 | 3 | 4 | 5 | 👨‍💼 Great! Now we can create DOM nodes dynamically using JavaScript. This is only 6 | the beginning of our journey. Creating DOM nodes ourselves is not typically the 7 | best way to build a full fledged application, but it's important for you to 8 | understand that what libraries like React are doing is not magic. They're just 9 | creating and modifying DOM nodes using JavaScript. 10 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/02.solution.root/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /exercises/04.components/01.solution.function/README.mdx: -------------------------------------------------------------------------------- 1 | # Simple Function 2 | 3 | 4 | 5 | 👨‍💼 Super, but that's definitely not the most fun way to use custom components. 6 | In fact, we're technically not creating a custom component at all. We're just 7 | interpolating a function call into our template. This will have interesting 8 | implications in the future with React features like state and effects. 9 | 10 | Instead, let's create React elements out of this function which is what we're 11 | going for... 12 | -------------------------------------------------------------------------------- /exercises/04.components/02.solution.raw/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep('message function is passed to createElement', async () => { 14 | expect(inlineScript.textContent).to.include('createElement(message') 15 | }) 16 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.solution.spread/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep('props are spread on the div', async () => { 14 | expect(inlineScript.textContent, 'props should be spread').to.include( 15 | '...props', 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /exercises/04.components/03.solution.jsx/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep( 14 | 'Correctly creating a component with JSX', 15 | async () => { 16 | expect(inlineScript.textContent).to.include(' 2 | 3 |
4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/04.problem.nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Nesting JSX 2 | 3 | 4 | 5 | 👨‍💼 Remember when we created this with `createElement`? 6 | 7 | ```html 8 |
9 |

Here's Sam's favorite food:

10 |
    11 |
  • Green eggs
  • 12 |
  • Ham
  • 13 |
14 |
15 | ``` 16 | 17 | That was... intellectually stimulating 😅. Well, now try doing the same thing 18 | using JSX. I promise it'll be more fun this time. 19 | 20 | 21 | 💰 Tip: remember `class` in JSX is `className`. 22 | 23 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.solution.compiler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /exercises/04.components/04.problem.props/README.mdx: -------------------------------------------------------------------------------- 1 | # Props 2 | 3 | 4 | 5 | 👨‍💼 You are the commander of your component's API. The API for your component is 6 | "props" which is the object your component function accepts as an argument. 7 | 8 | So now, we're going to use a `Calculator` component that can display an 9 | equation and its solution, like so: 10 | 11 | ```tsx 12 | element = 13 | // should render: 14 | //
15 | // 16 | // 1 + 2 = 3 17 | // 18 | //
19 | ``` 20 | 21 | Let's get some more practice with custom components. 22 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.solution.nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.solution.spread/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.solution.hello/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.solution.compiler/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.solution.interpolation/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.problem.compiler/README.mdx: -------------------------------------------------------------------------------- 1 | # Compiling JSX 2 | 3 | 4 | 5 | 6 | Normally you'll compile all of your code at build-time before you ship your 7 | application to the browser, but because Babel is written in JavaScript we can 8 | actually run it _in_ the browser to compile our code on the fly and that's 9 | what we'll do in this exercise. 10 | 11 | 12 | 👨‍💼 Let's add a script tag for Babel, then we'll update our own script tag to 13 | tell Babel to compile it for us on the fly. When you're done, you should notice 14 | the compiled version of the code appears in the `` of the DOM (which you 15 | can inspect using DevTools). 16 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.solution.interpolation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.solution.spread/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /epicshop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-bookworm-slim as base 2 | 3 | RUN apt-get update && apt-get install -y git 4 | 5 | ENV EPICSHOP_GITHUB_REPO=https://github.com/epicweb-dev/get-started-with-react 6 | ENV EPICSHOP_CONTEXT_CWD="/myapp/workshop-content" 7 | ENV EPICSHOP_HOME_DIR="/myapp/.epicshop" 8 | ENV EPICSHOP_DEPLOYED="true" 9 | ENV EPICSHOP_DISABLE_WATCHER="true" 10 | ENV FLY="true" 11 | ENV PORT="8080" 12 | ENV NODE_ENV="production" 13 | 14 | WORKDIR /myapp 15 | 16 | # Clone the workshop repo during build time, excluding database files 17 | RUN git clone --depth 1 ${EPICSHOP_GITHUB_REPO} ${EPICSHOP_CONTEXT_CWD} 18 | 19 | ADD . . 20 | 21 | RUN npm install --omit=dev 22 | 23 | RUN cd ${EPICSHOP_CONTEXT_CWD} && \ 24 | npx epicshop warm 25 | 26 | CMD cd ${EPICSHOP_CONTEXT_CWD} && \ 27 | npx epicshop start 28 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.problem.interpolation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.solution.fragments/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.problem.spread/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/04.solution.nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /exercises/02.raw-react/03.solution.deep-nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/02.problem.root/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.solution.fragments/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep('fragments are used', async () => { 14 | const usesShorthand = inlineScript.textContent?.includes('<>') 15 | const usesLonghand = inlineScript.textContent?.includes('') 16 | const usesFragment = inlineScript.textContent?.includes('') 17 | expect( 18 | usesShorthand || usesLonghand || usesFragment, 19 | 'fragments are not used. You must use <>, , or ', 20 | ).to.be.true 21 | }) 22 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.solution.interpolation/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep('className is interpolated', async () => { 14 | expect(inlineScript.textContent).to.include('className={') 15 | expect(inlineScript.textContent).not.to.include('className="') 16 | }) 17 | 18 | await testStep('children is interpolated', async () => { 19 | expect( 20 | inlineScript.textContent, 21 | 'children should be interpolated', 22 | ).not.to.include('>Hello World<') 23 | expect(inlineScript.textContent).to.include('{children}') 24 | }) 25 | -------------------------------------------------------------------------------- /exercises/04.components/01.solution.function/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | if (!inlineScript) { 10 | throw new Error('inlineScript not found') 11 | } 12 | 13 | await testStep('message function is created', async () => { 14 | const functionForm = inlineScript.textContent?.includes('function message') 15 | const declarationForm = inlineScript.textContent?.includes('const message = ') 16 | expect(functionForm || declarationForm, 'message function must be declared') 17 | .to.be.true 18 | }) 19 | 20 | await testStep('message function is called', async () => { 21 | expect(inlineScript.textContent).to.include('message(') 22 | }) 23 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.problem.fragments/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /exercises/04.components/03.solution.jsx/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.problem.nesting/README.mdx: -------------------------------------------------------------------------------- 1 | # Nesting Elements 2 | 3 | 4 | 5 | 👨‍💼 See if you can figure out how to write the JavaScript + React code to 6 | generate this DOM output: 7 | 8 | ```html 9 |
10 | Hello 11 | World 12 |
13 | ``` 14 | 15 | Hint: You can either use the `children` prop or additional arguments after the 16 | props. If you use the `children` prop, you will get a warning in the developer 17 | console about needing a "key" prop. We'll get to that later. 18 | 19 | Tip: You'll use `createElement` for the `span` elements, but to get the 20 | space between them, you'll need to use a string (`' '`) to tell React to create 21 | a [`textNode`](https://developer.mozilla.org/en-US/docs/Web/API/Text). 22 | 23 | - [📜 `createElement`](https://react.dev/reference/react/createElement#createelement) 24 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/04.problem.nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/03.problem.spread/README.mdx: -------------------------------------------------------------------------------- 1 | # Spread props 2 | 3 | 4 | 5 | What if I have an object of props that I want applied to the `div` like this: 6 | 7 | ```tsx 8 | const children = 'Hello World' 9 | const className = 'container' 10 | const props = { children, className } 11 | const element =
// how do I apply props to this div? 12 | ``` 13 | 14 | If we were doing raw React APIs it would be: 15 | 16 | ```tsx 17 | const element = createElement('div', props) 18 | ``` 19 | 20 | Or, it could be written like this: 21 | 22 | ```tsx 23 | const element = createElement('div', { ...props }) 24 | ``` 25 | 26 | 👨‍💼 See if you can figure out how to make that work with JSX. Take an object of 27 | props, and apply those props to a React element. 28 | 29 | 📜 30 | [Forwarding props with the JSX spread syntax](https://react.dev/learn/passing-props-to-a-component#forwarding-props-with-the-jsx-spread-syntax) 31 | -------------------------------------------------------------------------------- /exercises/04.components/01.solution.function/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /exercises/04.components/02.solution.raw/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | const messages = Array.from(element.querySelectorAll('.message')) 22 | expect(messages, 'messages not found').to.have.length(2) 23 | const [helloMessage, goodbyeMessage] = messages 24 | expect(helloMessage.textContent).to.equal('Hello World') 25 | expect(goodbyeMessage.textContent).to.equal('Goodbye World') 26 | }) 27 | -------------------------------------------------------------------------------- /exercises/04.components/03.solution.jsx/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | const messages = Array.from(element.querySelectorAll('.message')) 22 | expect(messages, 'messages not found').to.have.length(2) 23 | const [helloMessage, goodbyeMessage] = messages 24 | expect(helloMessage.textContent).to.equal('Hello World') 25 | expect(goodbyeMessage.textContent).to.equal('Goodbye World') 26 | }) 27 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /exercises/04.components/01.solution.function/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | const messages = Array.from(element.querySelectorAll('.message')) 22 | expect(messages, 'messages not found').to.have.length(2) 23 | const [helloMessage, goodbyeMessage] = messages 24 | expect(helloMessage.textContent).to.equal('Hello World') 25 | expect(goodbyeMessage.textContent).to.equal('Goodbye World') 26 | }) 27 | -------------------------------------------------------------------------------- /epicshop/setup-custom.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { 3 | getApps, 4 | isProblemApp, 5 | setPlayground, 6 | } from '@epic-web/workshop-utils/apps.server' 7 | import { warm } from 'epicshop/warm' 8 | import fsExtra from 'fs-extra' 9 | 10 | await warm() 11 | 12 | const allApps = await getApps() 13 | const problemApps = allApps.filter(isProblemApp) 14 | 15 | if (!process.env.SKIP_PLAYGROUND) { 16 | const firstProblemApp = problemApps[0] 17 | if (firstProblemApp) { 18 | console.log('🛝 setting up the first problem app...') 19 | const playgroundPath = path.join(process.cwd(), 'playground') 20 | if (await fsExtra.exists(playgroundPath)) { 21 | console.log('🗑 deleting existing playground app') 22 | await fsExtra.remove(playgroundPath) 23 | } 24 | await setPlayground(firstProblemApp.fullPath).then( 25 | () => { 26 | console.log('✅ first problem app set up') 27 | }, 28 | (error) => { 29 | console.error(error) 30 | throw new Error('❌ first problem app setup failed') 31 | }, 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /epicshop/playwright.config.js: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import path from 'path' 3 | import { defineConfig, devices } from '@playwright/test' 4 | 5 | const PORT = process.env.PORT || '5639' 6 | const tmpDir = path.join( 7 | os.tmpdir(), 8 | 'epicshop-playwright', 9 | path.basename(new URL('../', import.meta.url).pathname), 10 | ) 11 | 12 | export default defineConfig({ 13 | workers: process.env.CI ? 1 : undefined, 14 | outputDir: path.join(tmpDir, 'playwright-test-output'), 15 | reporter: [ 16 | [ 17 | 'html', 18 | { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, 19 | ], 20 | ], 21 | use: { 22 | baseURL: `http://localhost:${PORT}/`, 23 | trace: 'retain-on-failure', 24 | }, 25 | 26 | projects: [ 27 | { 28 | name: 'chromium', 29 | use: { ...devices['Desktop Chrome'] }, 30 | }, 31 | ], 32 | 33 | webServer: { 34 | command: 'cd .. && npm start', 35 | port: Number(PORT), 36 | reuseExistingServer: !process.env.CI, 37 | stdout: 'pipe', 38 | stderr: 'pipe', 39 | env: { PORT }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.problem.nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /exercises/04.components/02.solution.raw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /exercises/02.raw-react/03.problem.deep-nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /exercises/04.components/01.problem.function/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /exercises/04.components/03.problem.jsx/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /exercises/04.components/02.problem.raw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /exercises/04.components/04.solution.props/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = rootElement 9 | 10 | await dtl.waitFor( 11 | () => { 12 | const h1 = element.querySelector('h1') 13 | expect(h1, 'h1 not found').to.be.instanceOf(HTMLElement) 14 | expect(h1!.textContent).to.equal('Calculator') 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | const codeElements = Array.from(element.querySelectorAll('code')) 20 | expect(codeElements, 'code elements not found').to.have.length(4) 21 | 22 | const [plus, minus, times, divide] = codeElements 23 | 24 | expect(plus?.textContent).to.equal('1 + 2 = 3') 25 | expect(minus?.textContent).to.equal('1 - 2 = -1') 26 | expect(times?.textContent).to.equal('1 * 2 = 2') 27 | expect(divide?.textContent).to.equal('1 / 2 = 0.5') 28 | }) 29 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.problem.hello/README.mdx: -------------------------------------------------------------------------------- 1 | # Hello World in JS 2 | 3 | 4 | 5 | 👨‍💼 It's important to have a basic understanding of how to generate and interact 6 | with DOM nodes using JavaScript because it will help you understand how React 7 | works under the hood a little better. So in this exercise we're actually not 8 | going to use React at all. Instead we're going to use JavaScript to create a 9 | `div` DOM node with the text "Hello World" and insert that DOM node into the 10 | document. 11 | 12 | 🐨 We'll be in to help guide you through making 13 | this work! See you there! 14 | 15 | 👨‍💼 Once you're finished, open up the 16 | [browser devtools](https://developer.chrome.com/docs/devtools) so 17 | you can check the DOM is what you expect it to be. 18 | 19 | {/* prettier-ignore */} 20 | 21 | 💡 Tip: you may find it useful to{' '}open the playground in a separate tab{'.'} 22 | 23 | -------------------------------------------------------------------------------- /epicshop/fly.yaml: -------------------------------------------------------------------------------- 1 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 2 | # 3 | 4 | app: 'epicweb-dev-get-started-with-react' 5 | primary_region: sjc 6 | kill_signal: SIGINT 7 | kill_timeout: 5s 8 | swap_size_mb: 512 9 | 10 | experimental: 11 | auto_rollback: true 12 | 13 | attached: 14 | secrets: {} 15 | 16 | services: 17 | - processes: 18 | - app 19 | protocol: tcp 20 | internal_port: 8080 21 | 22 | ports: 23 | - port: 80 24 | 25 | handlers: 26 | - http 27 | force_https: true 28 | - port: 443 29 | 30 | handlers: 31 | - tls 32 | - http 33 | 34 | concurrency: 35 | type: connections 36 | hard_limit: 100 37 | soft_limit: 80 38 | 39 | tcp_checks: 40 | - interval: 15s 41 | timeout: 2s 42 | grace_period: 1s 43 | 44 | http_checks: 45 | - interval: 10s 46 | timeout: 2s 47 | grace_period: 5s 48 | method: get 49 | path: /resources/healthcheck 50 | protocol: http 51 | tls_skip_verify: false 52 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/02.solution.root/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | 23 | await testStep('root element is not in the HTML', async () => { 24 | const response = await fetch(location.href) 25 | const text = await response.text() 26 | const node = document.createElement('div') 27 | node.innerHTML = text 28 | expect( 29 | node.querySelector('#root'), 30 | 'root element found in the HTML when it should not be', 31 | ).to.be.null 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/04.components/02.problem.raw/README.mdx: -------------------------------------------------------------------------------- 1 | # Raw API 2 | 3 | 4 | 5 | 👨‍💼 So far we've only used `createElement('someString')`, but the first 6 | argument to `createElement` can also be a function which returns something 7 | that's renderable. 8 | 9 | So instead of calling your `message` function, pass it as the first argument to 10 | `createElement` and pass the `{children: 'Hello World'}` object as the 11 | second argument. 12 | 13 | ```ts 14 | createElement( 15 | someFunction, 16 | { prop1: 'value1', prop2: 'value2' }, 17 | 'child1', 18 | 'child2', 19 | ) 20 | ``` 21 | 22 | Then `someFunction` will be called with the props object as the first argument 23 | and the children will appear as an array in the children property of the props 24 | object. 25 | 26 | ```ts 27 | function someFunction(props) { 28 | props.children // ['child1', 'child2'] 29 | props.prop1 // 'value1' 30 | props.prop2 // 'value2' 31 | return // some jsx 32 | } 33 | ``` 34 | 35 | So let's move from calling `message` directly to calling it through 36 | `createElement`. 37 | -------------------------------------------------------------------------------- /exercises/04.components/01.problem.function/README.mdx: -------------------------------------------------------------------------------- 1 | # Simple Function 2 | 3 | 4 | 5 | 👨‍💼 The DOM we want to generate is like this: 6 | 7 | ```html 8 |
9 |
Hello World
10 |
Goodbye World
11 |
12 | ``` 13 | 14 | In this case, it would be cool if we could reduce the duplication for creating 15 | the React elements for this: 16 | 17 | ```tsx 18 |
{children}
19 | ``` 20 | 21 | So we need to make a function which accepts an object argument with a `children` 22 | property and returns the React element. Then you can interpolate a call to that 23 | function in your JSX. 24 | 25 | ```tsx 26 |
{message('Hello World')}
27 | ``` 28 | 29 | This is not how we write custom React components, but this is important for you 30 | to understand them. We'll get to custom components in the next steps. 31 | 32 | 📜 Read more 33 | 34 | - [JavaScript in JSX with Curly Braces](https://react.dev/learn/javascript-in-jsx-with-curly-braces) 35 | - [What is JSX?](https://kentcdodds.com/blog/what-is-jsx) 36 | -------------------------------------------------------------------------------- /exercises/02.raw-react/01.solution.elements/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | 7 | const element = await dtl.waitFor( 8 | async () => { 9 | const element = rootElement!.querySelector('.container') 10 | expect(element, 'container element not found').to.be.instanceOf( 11 | HTMLElement, 12 | ) 13 | return element 14 | }, 15 | { timeout: 5000 }, 16 | ) 17 | 18 | expect(element!.textContent, 'element text is not correct').to.equal( 19 | 'Hello World', 20 | ) 21 | }) 22 | 23 | await testStep('The DOM element is created by React', () => { 24 | const element = document.querySelector('#root .container') 25 | expect(element, 'element not found').to.be.instanceOf(HTMLElement) 26 | if (!element) return 27 | 28 | const reactKeys = Object.keys(element).filter((key) => 29 | key.startsWith('__react'), 30 | ) 31 | expect( 32 | reactKeys.length, 33 | 'element was not created by React', 34 | ).to.be.greaterThan(0) 35 | }) 36 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/02.problem.interpolation/README.mdx: -------------------------------------------------------------------------------- 1 | # Interpolation 2 | 3 | 4 | 5 | 🦉 "Interpolation" is defined as "the insertion of something of a different 6 | nature into something else." 7 | 8 | Let's take template literals for example: 9 | 10 | ```typescript 11 | const greeting = 'Sup' 12 | const subject = 'World' 13 | const message = `${greeting} ${subject}` 14 | ``` 15 | 16 | 👨‍💼 See if you can figure out how to extract the `className` (`"container"`) and 17 | `children` (`"Hello World"`) to variables and interpolate them in the JSX. 18 | 19 | ```tsx 20 | const className = 'container' 21 | const children = 'Hello World' 22 | const element =
how do I make this work?
23 | ``` 24 | 25 | 📜 The React docs for JSX are pretty good: 26 | [Writing Markup with JSX](https://react.dev/learn/writing-markup-with-jsx) 27 | 28 | Here are a few sections of particular interest for this step: 29 | 30 | - [JavaScript in JSX with Curly Braces](https://react.dev/learn/javascript-in-jsx-with-curly-braces) 31 | - [Passing strings with quotes](https://react.dev/learn/javascript-in-jsx-with-curly-braces#passing-strings-with-quotes) 32 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/README.mdx: -------------------------------------------------------------------------------- 1 | # Using JSX 2 | 3 | 4 | 5 | JSX is more intuitive than the raw React API and is easier to understand when 6 | reading the code. It's fairly simple HTML-like syntactic sugar on top of the raw 7 | React APIs: 8 | 9 | ```tsx 10 | const element =

Hey there

11 | 12 | // ↓ ↓ ↓ ↓ compiles to ↓ ↓ ↓ ↓ 13 | 14 | const element = createElement('h1', { 15 | id: 'greeting', 16 | children: 'Hey there', 17 | }) 18 | ``` 19 | 20 | Because JSX is not actually JavaScript, you have to convert it using something 21 | called a code compiler. [Babel](https://babeljs.io) is one such tool. 22 | 23 | 24 | 🦉 Pro tip: If you'd like to see how JSX gets compiled to JavaScript, [check 25 | out the online babel REPL 26 | here](https://babeljs.io/repl#?builtIns=App&code_lz=MYewdgzgLgBArgSxgXhgHgCYIG4D40QAOAhmLgBICmANtSGgPRGm7rNkDqIATtRo-3wMseAFBA&presets=react&prettier=true). 27 | 28 | 29 | If you can train your brain to look at JSX and see the compiled version of that 30 | code, you'll be MUCH more effective at reading and using it! I strongly 31 | recommend you give this some intentional practice. 32 | -------------------------------------------------------------------------------- /exercises/04.components/README.mdx: -------------------------------------------------------------------------------- 1 | # Custom Components 2 | 3 | 4 | 5 | Just like in regular JavaScript, when you want to reuse code, you write 6 | functions. If you want to share JSX, you can do that as well. In React we call 7 | these functions "components" and they have some special properties. 8 | 9 | Components are functions which accept an object called "props" and return 10 | something that is renderable (more React elements, strings, `null`, numbers, 11 | etc.). To be clear, this is _the_ definition of a React component. That's all it 12 | is. So I'll say it 13 | [again](https://twitter.com/kentcdodds/status/1763606427028136131): 14 | 15 | > Components are functions which accept an object called "props" and return 16 | > something that is renderable 17 | 18 | Here's an example of that: 19 | 20 | ```tsx 21 | function Greeting(props) { 22 | return

Hello, {props.name}

23 | } 24 | ``` 25 | 26 | Then that component can be used like this: 27 | 28 | ```tsx 29 | 30 | ``` 31 | 32 | Just like previous exercises, we're going to ease into this syntax so you have a 33 | solid foundational understanding of how this works. 34 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.solution.fragments/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = rootElement 9 | 10 | const p = await dtl.waitFor( 11 | async () => { 12 | const p = element.querySelector('p') 13 | expect(p, '

not found').to.be.instanceOf(HTMLElement) 14 | return p 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | const ul = element.querySelector('ul') 20 | expect(ul, '

    not found').to.be.instanceOf(HTMLElement) 21 | expect(ul).to.have.class('sams-food') 22 | const lis = element.querySelectorAll('li') 23 | expect(lis, '
  • elements not found').to.have.length(2) 24 | 25 | expect(p!.textContent, 'p text is not correct').to.equal( 26 | "Here's Sam's favorite food:", 27 | ) 28 | 29 | const [greenEggs, ham] = ul!.querySelectorAll('li') 30 | 31 | expect(greenEggs.textContent, 'green eggs text is not correct').to.equal( 32 | 'Green eggs', 33 | ) 34 | expect(ham.textContent, 'ham text is not correct').to.equal('Ham') 35 | }) 36 | -------------------------------------------------------------------------------- /exercises/02.raw-react/01.problem.elements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.solution.nesting/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('"Hello World" is rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | if (element.textContent === 'HelloWorld') { 22 | throw new Error( 23 | 'Looks like you forgot to include the space between the spans', 24 | ) 25 | } 26 | 27 | expect(element.textContent, 'element text is not correct').to.equal( 28 | 'Hello World', 29 | ) 30 | 31 | const [hello, world] = element.querySelectorAll('span') 32 | 33 | expect(hello, 'Hello span not found').to.be.instanceOf(HTMLElement) 34 | expect(world, 'World span not found').to.be.instanceOf(HTMLElement) 35 | 36 | expect(hello.textContent, 'hello text is not correct').to.equal('Hello') 37 | expect(world.textContent, 'world text is not correct').to.equal('World') 38 | }) 39 | -------------------------------------------------------------------------------- /exercises/02.raw-react/03.solution.deep-nesting/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | const p = element.querySelector('p') 22 | expect(p, '

    not found').to.be.instanceOf(HTMLElement) 23 | const ul = element.querySelector('ul') 24 | expect(ul, '

      not found').to.be.instanceOf(HTMLElement) 25 | const lis = element.querySelectorAll('li') 26 | expect(lis, '
    • elements not found').to.have.length(2) 27 | 28 | expect(p!.textContent, 'p text is not correct').to.equal( 29 | "Here's Sam's favorite food:", 30 | ) 31 | 32 | const [greenEggs, ham] = ul!.querySelectorAll('li') 33 | 34 | expect(greenEggs.textContent, 'green eggs text is not correct').to.equal( 35 | 'Green eggs', 36 | ) 37 | expect(ham.textContent, 'ham text is not correct').to.equal('Ham') 38 | }) 39 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.solution.compiler/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | const response = await fetch(location.href) 4 | const indexHtml = await response.text() 5 | const node = document.createElement('div') 6 | node.innerHTML = indexHtml 7 | 8 | const inlineScript = node.querySelector('script[type="text/babel"]') 9 | await testStep('script with type text/babel exists', () => { 10 | expect( 11 | inlineScript, 12 | 'script with type text/babel not found', 13 | ).to.be.instanceOf(HTMLScriptElement) 14 | }) 15 | 16 | await testStep('script with type text/babel has data-type="module"', () => { 17 | expect( 18 | inlineScript!.getAttribute('data-type'), 19 | 'script with type text/babel does not have data-type="module"', 20 | ).to.equal('module') 21 | }) 22 | 23 | await testStep('babel is loaded', () => { 24 | const script = node.querySelector('script[src="/babel-standalone.js"]') 25 | expect( 26 | script, 27 | 'babel script not found, did you remember to add a script for "/babel-standalone.js" as the src?', 28 | ).to.be.instanceOf(HTMLScriptElement) 29 | }) 30 | 31 | await testStep('JSX is in use', async () => { 32 | expect( 33 | inlineScript!.textContent, 34 | 'createElement( should not appear in your source', 35 | ).not.to.include('createElement(') 36 | expect(inlineScript!.textContent, 'JSX is not in use').to.include('
') 37 | }) 38 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/04.solution.nesting/content.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('Proper elements are rendered to the DOM', async () => { 4 | const rootElement = document.getElementById('root') 5 | expect(rootElement, 'root element not found').to.be.instanceOf(HTMLElement) 6 | if (!rootElement) return 7 | 8 | const element = await dtl.waitFor( 9 | () => { 10 | const element = rootElement!.querySelector('.container') 11 | expect(element, 'container element not found').to.be.instanceOf( 12 | HTMLElement, 13 | ) 14 | return element 15 | }, 16 | { timeout: 5000 }, 17 | ) 18 | 19 | if (!element) return 20 | 21 | const p = element.querySelector('p') 22 | expect(p, '

not found').to.be.instanceOf(HTMLElement) 23 | const ul = element.querySelector('ul') 24 | expect(ul, '

    not found').to.be.instanceOf(HTMLElement) 25 | expect(ul).to.have.class('sams-food') 26 | const lis = element.querySelectorAll('li') 27 | expect(lis, '
  • elements not found').to.have.length(2) 28 | 29 | expect(p!.textContent, 'p text is not correct').to.equal( 30 | "Here's Sam's favorite food:", 31 | ) 32 | 33 | const [greenEggs, ham] = ul!.querySelectorAll('li') 34 | 35 | expect(greenEggs.textContent, 'green eggs text is not correct').to.equal( 36 | 'Green eggs', 37 | ) 38 | expect(ham.textContent, 'ham text is not correct').to.equal('Ham') 39 | }) 40 | -------------------------------------------------------------------------------- /exercises/04.components/04.solution.props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /exercises/04.components/04.problem.props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.problem.hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'main' 11 | pull_request: 12 | branches: 13 | - 'main' 14 | jobs: 15 | setup: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: ⬇️ Checkout repo 22 | uses: actions/checkout@v4 23 | 24 | - name: ⎔ Setup node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | 29 | - name: ▶️ Run setup script 30 | run: npm run setup 31 | 32 | - name: ⬣ ESLint 33 | run: npm run lint 34 | 35 | # TODO: get this working again 36 | # - name: ⬇️ Install Playwright 37 | # run: npm --prefix epicshop run test:setup 38 | 39 | # - name: 🧪 In-browser tests 40 | # run: npm --prefix epicshop test 41 | 42 | deploy: 43 | name: 🚀 Deploy 44 | runs-on: ubuntu-latest 45 | # only deploy main branch on pushes 46 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} 47 | 48 | steps: 49 | - name: ⬇️ Checkout repo 50 | uses: actions/checkout@v4 51 | 52 | - name: 🎈 Setup Fly 53 | uses: superfly/flyctl-actions/setup-flyctl@1.5 54 | 55 | - name: 🚀 Deploy 56 | run: flyctl deploy --remote-only 57 | working-directory: ./epicshop 58 | env: 59 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 60 | -------------------------------------------------------------------------------- /exercises/README.mdx: -------------------------------------------------------------------------------- 1 | # Get Started with React 🏃‍➡️ 2 | 3 | 4 | 5 | 👨‍💼 Hello! I'm Peter the Project Manager and I'm here to help you know what our 6 | users want you to know so you can build the kind of React application they need! 7 | In this workshop we're going to give you the foundational knowledge you need 8 | upon which you can build your React skills. 9 | 10 | Our users don't need much at the start of this workshop ("Hello World" is about 11 | as far as we'll get for a bit), but they'll need custom components with props 12 | soon which are the building blocks of React applications. We'll cover all of 13 | that and more in this series of exercises. 14 | 15 | It's important that you understand the fundamentals of React so you can not just 16 | use React, but _understand_ it. This will help you build better applications and 17 | be more effective at debugging when things go wrong. 18 | 19 | With that, let's get started! 20 | 21 | 22 | NOTE: all the exercises in this workshop are in `index.html` files to keep 23 | things as simple as possible. The JavaScript you write will be inside inline 24 | ` 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.solution.compiler/README.mdx: -------------------------------------------------------------------------------- 1 | # Compiling JSX 2 | 3 | 4 | 5 | 🦉 Let's break down the key changes in our `script` tag: 6 | 7 | 1. `type="text/babel"` and `data-type="module"`: 8 | - `type="text/babel"` tells Babel to transpile this script, allowing us to use JSX and modern JavaScript features. 9 | - `data-type="module"` indicates that this script should be treated as a module after Babel transpilation. This enables us to use `import` statements. 10 | 11 | We use both instead of just `type="module"` because browsers don't natively understand JSX. Babel needs to transform our code first, then it can be treated as a module. 12 | 13 | 2. Importing React: 14 | We've changed from: 15 | 16 | ```javascript 17 | import { createElement } from '/react.js' 18 | ``` 19 | 20 | to: 21 | 22 | ```javascript 23 | import * as React from '/react.js' 24 | ``` 25 | 26 | This imports all exports from React as a namespace called `React`. It's beneficial because: 27 | - It provides access to all React APIs, not just `createElement`. 28 | - When using JSX, the transpiler will convert JSX elements to `React.createElement` calls, so we need the `React` namespace in scope. 29 | 30 | 🔍 To dive deeper into these concepts, check out these resources: 31 | 32 | - [📜 Babel documentation on browser usage](https://babeljs.io/docs/babel-standalone) 33 | - [📜 React documentation on JSX](https://react.dev/learn/writing-markup-with-jsx) 34 | - [📜 MDN on JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 35 | 36 | 👨‍💼 Great! Now we're ready to start using JSX in our HTML file! 37 | -------------------------------------------------------------------------------- /exercises/04.components/03.solution.jsx/README.mdx: -------------------------------------------------------------------------------- 1 | # JSX Components 2 | 3 | 4 | 5 | 👨‍💼 Great job! We can now use JSX to write custom components and reuse bits of 6 | our UI with ease. 7 | 8 | 🦉 The children prop is special because it can appear either as a prop or in 9 | between the opening and closing tags of a component. So these two are 10 | equivalent: 11 | 12 | ```tsx 13 | Something went wrong! 14 | 15 | ``` 16 | 17 | Our `Message` component uses the "special" and implicit `children` prop. It's 18 | special because it means we can do this: 19 | 20 | ```tsx 21 | element = Hello World 22 | // is functionally equivalent to: 23 | element = 24 | ``` 25 | 26 | And you can put JSX in the children prop with either syntax as well: 27 | 28 | ```tsx 29 | element = ( 30 | 31 | Hello World 32 | 33 | ) 34 | // is functionally equivalent to: 35 | element = Hello, ' ', World]} /> 36 | ``` 37 | 38 | 📜 [Learn more about passing JSX as `children`](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children). 39 | 40 | But we don't have to use the `children` prop, we can call it whatever we want. 41 | So you could also do: 42 | 43 | ```tsx 44 | element = Hello, ' ', World]} /> 45 | ``` 46 | 47 | The only thing that's special about the `children` prop is that it's implicit 48 | in JSX. 49 | 50 | And sometimes using something other than the `children` prop can be really 51 | useful, but we'll get to that in a future workshop. 52 | -------------------------------------------------------------------------------- /epicshop/setup.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process' 2 | 3 | const styles = { 4 | // got these from playing around with what I found from: 5 | // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96 6 | // they're the best I could find that works well for light or dark terminals 7 | success: { open: '\u001b[32;1m', close: '\u001b[0m' }, 8 | danger: { open: '\u001b[31;1m', close: '\u001b[0m' }, 9 | info: { open: '\u001b[36;1m', close: '\u001b[0m' }, 10 | subtitle: { open: '\u001b[2;1m', close: '\u001b[0m' }, 11 | } 12 | 13 | function color(modifier, string) { 14 | return styles[modifier].open + string + styles[modifier].close 15 | } 16 | 17 | console.log(color('info', '▶️ Starting workshop setup...')) 18 | 19 | const output = spawnSync('npm --version', { shell: true }) 20 | .stdout.toString() 21 | .trim() 22 | const outputParts = output.split('.') 23 | const major = Number(outputParts[0]) 24 | const minor = Number(outputParts[1]) 25 | if (major < 8 || (major === 8 && minor < 16)) { 26 | console.error( 27 | color( 28 | 'danger', 29 | '🚨 npm version is ' + 30 | output + 31 | ' which is out of date. Please install npm@8.16.0 or greater', 32 | ), 33 | ) 34 | throw new Error('npm version is out of date') 35 | } 36 | 37 | const command = 38 | 'npx --yes "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q' 39 | console.log( 40 | color('subtitle', ' Running the following command: ' + command), 41 | ) 42 | 43 | const result = spawnSync(command, { stdio: 'inherit', shell: true }) 44 | 45 | if (result.status === 0) { 46 | console.log(color('success', '✅ Workshop setup complete...')) 47 | } else { 48 | process.exit(result.status) 49 | } 50 | 51 | /* 52 | eslint 53 | "no-undef": "off", 54 | "vars-on-top": "off", 55 | */ 56 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/05.problem.fragments/README.mdx: -------------------------------------------------------------------------------- 1 | # Fragments 2 | 3 | 4 | 5 | 🦉 One feature of JSX that you'll find useful is called ["React Fragments"](https://react.dev/reference/react/Fragment). 6 | 7 | React Fragments allow you to group multiple elements without adding an extra 8 | DOM node. This lets you return multiple elements side-by-side from a component 9 | without needing a wrapper div. It's useful for avoiding unnecessary markup 10 | that could affect styling or layout. 11 | 12 | We have currently the following code: 13 | 14 | ```html 15 |
    16 |

    Here's Sam's favorite food:

    17 |
      18 |
    • Green eggs
    • 19 |
    • Ham
    • 20 |
    21 |
    22 | ``` 23 | 24 | We want to do the same thing as above, except we don't want the `container` `div`. So, we want to just create: 25 | 26 | ```html 27 |

    Here's Sam's favorite food:

    28 |
      29 |
    • Green eggs
    • 30 |
    • Ham
    • 31 |
    32 | ``` 33 | 34 | In React, we do this with ``. Replace the 35 | `
    ` with a fragment and inspect the DOM to notice that 36 | the elements are both rendered as direct children of `root`. 37 | 38 | 39 | 💰 TIP: Fragments are common enough that there's a syntax shortcut for them. 40 | You can open with `<>` and close with ``, so: 41 | 42 | 43 | ```tsx 44 | element = this is in a fragment 45 | // is the same as: 46 | element = <>this is in a fragment 47 | ``` 48 | 49 | 👨‍💼 We want to get rid of the `
    ` we have in there, but 50 | we need to keep the `

    ` and `

      ` next to each other. Figure out how to make 51 | that work. 52 | 53 | 💯 As a little extra part of this, try to figure out why `` is 54 | needed at all. Might help if you look at what this looks like using 55 | `createElement` rather than JSX. As I said, understanding how JSX compiles 56 | to `createElement` really helps improve your capabilities with JSX! 57 | -------------------------------------------------------------------------------- /exercises/02.raw-react/README.mdx: -------------------------------------------------------------------------------- 1 | # Raw React APIs 2 | 3 | 4 | 5 | React is the most widely used frontend framework in the world and it's using the 6 | same APIs that you're using when it creates DOM nodes. 7 | 8 | 9 | In fact, [here's where that happens in the React source 10 | code](https://github.com/facebook/react/blob/fb10a2c66a923d218471b535fdaf0dbc530417ee/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L480) 11 | at the time of this writing. 12 | 13 | 14 | React abstracts away the imperative browser API from you to give you a much more 15 | declarative API to work with. 16 | 17 | 18 | Learn more about the difference between those two concepts here: [Imperative 19 | vs Declarative 20 | Programming](https://ui.dev/imperative-vs-declarative-programming/) 21 | 22 | 23 | One important thing to know about React is that it supports multiple platforms 24 | (for example, native mobile, native desktop, web, and even terminal and VR). 25 | Each of these platforms has its own code necessary for interacting with that 26 | platform, and then there's shared code between the platforms. 27 | 28 | With that in mind, you need two JavaScript files to write React applications for 29 | the web: 30 | 31 | - React: responsible for creating React elements (kinda like 32 | `document.createElement()`) 33 | - ReactDOM: responsible for rendering React elements to the DOM (kinda like 34 | `rootElement.append()`) 35 | 36 | You can learn more about this in 37 | [the react docs](https://react.dev/reference/react). 38 | 39 | We'll also be loading these files into our application from the `./public` 40 | directory. This works great for our purposes of keeping this as simple as 41 | possible so you can learn React and not get distracted by a bunch of tools 42 | (don't worry, those will come later). 43 | 44 | Finally, from now on, as you save your work, the browser will reload 45 | automatically thanks to the `epic_ws.js` file we'll be including at the top of 46 | the `index.html` file. You're welcome 😊. 47 | -------------------------------------------------------------------------------- /exercises/04.components/03.problem.jsx/README.mdx: -------------------------------------------------------------------------------- 1 | # JSX Components 2 | 3 | 4 | 5 | 👨‍💼 Rather than: 6 | 7 | ```tsx 8 | element = createElement(message, { children: 'Hello World' }) 9 | ``` 10 | 11 | I want to use JSX, even for custom components, like this: 12 | 13 | ```tsx 14 | element = Hello World 15 | ``` 16 | 17 | And we're so close! Just like using JSX for regular `div`s is nicer than using 18 | the raw `createElement` API, using JSX for custom components is nicer too. 19 | Remember that it's Babel that's responsible for taking our JSX and compiling it 20 | to `createElement` calls. If we try `Hello World`, 21 | here's what Babel will do: 22 | 23 | ```tsx 24 | element = Hello World 25 | 26 | // the desired output 27 | element = createElement(message, { children: 'Hello World' }) 28 | 29 | // the actual output 30 | element = createElement('message', { children: 'Hello World' }) 31 | ``` 32 | 33 | So we just need a way to tell Babel how to compile our JSX so it passes the 34 | function by its name rather than a string. We do this by how the JSX appears. 35 | Here are a few examples of Babel output for JSX: 36 | 37 | ```tsx 38 | element = // createElement(Capitalized) 39 | element = // createElement(property.access) 40 | element = // createElement(Property.Access) 41 | element = // SyntaxError 42 | element = // createElement('lowercase') 43 | element = // createElement('kebab-case') 44 | element = // createElement('Upper-Kebab-Case') 45 | element = // createElement(Upper_Snake_Case) 46 | element = // createElement('lower_snake_case') 47 | ``` 48 | 49 | Now let's refactor your function to a name that will make it possible to call it 50 | by using it as a JSX component. 51 | 52 | The most common approaches you'll find in the wild are to have the name of the 53 | component capitalized: 54 | 55 | ```tsx 56 | element = // createElement(Capitalized) 57 | ``` 58 | 59 | Sometimes you'll see the property access syntax as well. 60 | 61 | Let's go refactor so we can use our components as JSX! 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-with-react", 3 | "private": true, 4 | "epicshop": { 5 | "title": "Get Started with React 🏃‍➡️", 6 | "githubRoot": "https://github.com/epicweb-dev/get-started-with-react/blob/main", 7 | "subtitle": "Learn the foundational concepts necessary for building React applications and libraries", 8 | "stackBlitzConfig": { 9 | "view": "editor" 10 | }, 11 | "product": { 12 | "host": "www.epicreact.dev", 13 | "slug": "get-started-with-react", 14 | "displayName": "EpicReact.dev", 15 | "displayNameShort": "EpicReact", 16 | "logo": "/logo.svg", 17 | "discordChannelId": "1285244676286189569", 18 | "discordTags": [ 19 | "1285246046498328627", 20 | "1285245484432228375" 21 | ] 22 | }, 23 | "onboardingVideo": "https://www.epicweb.dev/tips/get-started-with-the-epic-workshop-app-for-react", 24 | "instructor": { 25 | "name": "Kent C. Dodds", 26 | "avatar": "/images/instructor.png", 27 | "𝕏": "kentcdodds" 28 | } 29 | }, 30 | "type": "module", 31 | "imports": { 32 | "#*": "./*" 33 | }, 34 | "prettier": "@epic-web/config/prettier", 35 | "scripts": { 36 | "postinstall": "cd ./epicshop && npm install", 37 | "start": "npx --prefix ./epicshop epicshop start", 38 | "dev": "npx --prefix ./epicshop epicshop start", 39 | "setup": "node ./epicshop/setup.js", 40 | "setup:custom": "node ./epicshop/setup-custom.js", 41 | "lint": "eslint .", 42 | "format": "prettier --write .", 43 | "typecheck": "tsc -b", 44 | "validate:all": "npm-run-all --parallel --print-label --print-name --continue-on-error lint typecheck" 45 | }, 46 | "keywords": [], 47 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 48 | "license": "GPL-3.0-only", 49 | "dependencies": { 50 | "react": "^19.1.0", 51 | "react-dom": "^19.1.0", 52 | "react-error-boundary": "^6.0.0" 53 | }, 54 | "devDependencies": { 55 | "@epic-web/config": "^1.21.0", 56 | "@epic-web/workshop-utils": "^6.47.6", 57 | "@types/react": "^19.1.8", 58 | "@types/react-dom": "^19.1.6", 59 | "eslint": "^9.31.0", 60 | "eslint-plugin-react-hooks": "^5.2.0", 61 | "npm-run-all": "^4.1.5", 62 | "prettier": "^3.6.2", 63 | "typescript": "^5.8.3" 64 | }, 65 | "engines": { 66 | "node": ">=20", 67 | "npm": ">=8.16.0", 68 | "git": ">=2.18.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/README.mdx: -------------------------------------------------------------------------------- 1 | # Hello World in JS 2 | 3 | 4 | 5 | It doesn't take long to learn how to make "Hello World" appear on the page with 6 | HTML: 7 | 8 | ```html 9 | 10 | 11 |
      Hello World
      12 | 13 | 14 | ``` 15 | 16 | The browser takes this HTML code and generates 17 | [the DOM (the Document Object Model)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) 18 | out of it. The browser then exposes the DOM to JavaScript so you can interact 19 | with it to add a layer of interactivity to your web-page. 20 | 21 | ```html 22 | 23 | 24 |
      Hello World
      25 | 28 | 29 | 30 | ``` 31 | 32 | Years ago, people were generating HTML on the server and then adding JavaScript 33 | on top of that generated HTML for interactivity. However, as requirements for 34 | that interactivity became more challenging, this approach produced applications 35 | that were difficult to maintain and had performance issues. 36 | 37 | 38 | If you'd like to learn more about the history, read [The Web's Next 39 | Transition](https://www.epicweb.dev/the-webs-next-transition). 40 | 41 | 42 | So modern JavaScript frameworks were created to address some of the challenges 43 | by programmatically creating the DOM rather than defining it in hand-written 44 | HTML. 45 | 46 | ```html 47 | 48 | 49 | 54 | 55 | 56 | ``` 57 | 58 | This approach is more flexible, but comes at the cost of making the browser do 59 | a little extra work. The browser has to parse the JavaScript code, execute it, 60 | and then generate the DOM from the JavaScript code before displaying things to 61 | the user. 62 | 63 | This is why hybrid approaches are popular. You can generate the DOM with HTML 64 | and then add interactivity with JavaScript. In the world of React, you can use 65 | React code to generate the HTML on the server and then use the same React code 66 | to add interactivity on the client. Doing this isn't an enormous amount of 67 | effort, however there are a lot of considerations to take into account so you'll 68 | want to use [Remix](https://remix.run) (which is built on top of React) to make 69 | it easier (and you get a lot of other critical pieces to the puzzle as well). 70 | 71 | We're going to focus on the client-side of things in this workshop, but you can 72 | apply the same principles to frameworks like Remix. But let's just start with 73 | the total fundamentals of creating and appending our own DOM nodes before we 74 | get into the React side of things. I call this "going down to level up." You'll 75 | find yourself much more efficient with React if you understand the fundamentals 76 | of the DOM and JavaScript. So let's get started! 77 | -------------------------------------------------------------------------------- /exercises/02.raw-react/01.problem.elements/README.mdx: -------------------------------------------------------------------------------- 1 | # Create React Elements 2 | 3 | 4 | 5 | 👨‍💼 Let's convert this to use React! But don't worry, we won't be doing any JSX just 6 | yet... You're going to use raw React APIs here. 7 | 8 | In modern applications, you typically get React and React DOM files from a "package registry", 9 | like [npm](https://npmjs.com) where packages we use are ([react](https://npm.im/react) and 10 | [react-dom](https://npm.im/react-dom)). 11 | 12 | 13 | A package registry is a centralized repository where developers can publish 14 | and share reusable code packages. 15 | 16 | 17 | To keep things simple for these exercises, we'll import React from slightly different locations: 18 | 19 | - `public/react.js` (accessed at `/react.js`) 20 | - `public/react-dom/client.js` (accessed at `/react-dom/client.js`) 21 | 22 | These files use a service called [esm.sh](https://esm.sh) to provide the React packages. 23 | 24 | Don't worry too much about [esm.sh](https://esm.sh) for now - it's just helping us load React easily in these exercises. 25 | 26 | Here's a simple example of how to use the React createElement API: 27 | 28 | ```typescript 29 | import { createElement } from '/react.js' 30 | import { createRoot } from '/react-dom/client.js' 31 | 32 | const elementProps = { id: 'element-id', children: 'Hello world!' } 33 | const elementType = 'h1' 34 | const reactElement = createElement(elementType, elementProps) 35 | 36 | const root = createRoot(rootElement) 37 | root.render(reactElement) 38 | ``` 39 | 40 | 41 | 42 | 🦉 As a reminder, in a typical application, your import will be something 43 | like 44 | 45 | ```ts nonumber nolang 46 | import { createRoot } from 'react-dom/client' 47 | ``` 48 | 49 | With that, a build tool or `importmap` will handle resolving that to the 50 | correct path. 51 | 52 | 53 | 54 | The "props" in `elementProps` above is short for "properties". Props are a key concept 55 | in React - they're the way we pass data into our elements. You can think of the element 56 | type as a blueprint for the kind of React component to create, and the props are the 57 | inputs that customize that element. 58 | 59 | `children` is a special prop in React. It represents the content inside an element. 60 | You have a few ways to specify children: 61 | 62 | 1. As a prop (like in the first example above) 63 | 2. As multiple arguments to `createElement` 64 | 3. As an array 65 | 66 | Here's an example showing the last two methods: 67 | 68 | ```typescript 69 | const elementProps = { id: 'element-id' } 70 | const elementType = 'h1' 71 | 72 | // Method 2: Multiple arguments 73 | const reactElement1 = createElement( 74 | elementType, 75 | elementProps, 76 | 'Hello', 77 | ' ', 78 | 'world!', 79 | ) 80 | 81 | // Method 3: Array of children 82 | const children = ['Hello', ' ', 'world!'] 83 | const reactElement2 = createElement(elementType, elementProps, children) 84 | 85 | createRoot(rootElement).render(reactElement1) // or reactElement2 86 | ``` 87 | 88 | Alright! Let's put this into practice! 89 | 90 | 91 | 💰 Tip: Use `console.log(reactElement)` to see what your created element looks 92 | like. You might find the structure interesting! 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
      2 |

      Get Started with React 🏃‍➡️

      3 | 4 | Learn the foundational concepts upon which all the rest of your React knowledge is built 5 | 6 |

      7 | Learn how to create React elements, render them to the page, and reuse them with custom components. This workshop will give you the foundational knowledge you need upon which you can build your React skills. 8 |

      9 |
      10 | 11 |
      12 | 13 |
      14 | 18 | 22 | 23 |
      24 | 25 |
      26 | 27 | 28 | [![Build Status][build-badge]][build] 29 | [![GPL 3.0 License][license-badge]][license] 30 | [![Code of Conduct][coc-badge]][coc] 31 | 32 | 33 | ## Prerequisites 34 | 35 | - Basic understanding of HTML, CSS, and JavaScript 36 | - Install the [React DevTools](https://react.dev/learn/react-developer-tools) 37 | 38 | ## Pre-workshop Resources 39 | 40 | Here are some resources you can read before taking the workshop to get you up to 41 | speed on some of the tools and concepts we'll be covering: 42 | 43 | - [JavaScript to Know for React](https://kentcdodds.com/blog/javascript-to-know-for-react) 44 | 45 | ## System Requirements 46 | 47 | - [git][git] v2.18 or greater 48 | - [NodeJS][node] v20 or greater 49 | - [npm][npm] v8 or greater 50 | 51 | All of these must be available in your `PATH`. To verify things are set up 52 | properly, you can run this: 53 | 54 | ```shell 55 | git --version 56 | node --version 57 | npm --version 58 | ``` 59 | 60 | If you have trouble with any of these, learn more about the PATH environment 61 | variable and how to fix it here for [windows][win-path] or 62 | [mac/linux][mac-path]. 63 | 64 | ## Setup 65 | 66 | Use the Epic Workshop CLI to get this setup: 67 | 68 | ```sh nonumber 69 | npx --yes epicshop@latest add get-started-with-react 70 | ``` 71 | 72 | If you experience errors here, please open [an issue][issue] with as many 73 | details as you can offer. 74 | 75 | ## The Workshop App 76 | 77 | Learn all about the workshop app on the 78 | [Epic Web Getting Started Guide](https://www.epicweb.dev/get-started). 79 | 80 | [![Kent with the workshop app in the background](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/280407082-0e012138-e01d-45d5-abf2-86ffe5d03c69.png)](https://www.epicweb.dev/get-started) 81 | 82 | 83 | [npm]: https://www.npmjs.com/ 84 | [node]: https://nodejs.org 85 | [git]: https://git-scm.com/ 86 | [build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/get-started-with-react/validate.yml?branch=main&logo=github&style=flat-square 87 | [build]: https://github.com/epicweb-dev/get-started-with-react/actions?query=workflow%3Avalidate 88 | [license-badge]: https://img.shields.io/badge/license-GPL%203.0%20License-blue.svg?style=flat-square 89 | [license]: https://github.com/epicweb-dev/get-started-with-react/blob/main/LICENSE 90 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 91 | [coc]: https://kentcdodds.com/conduct 92 | [win-path]: https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/ 93 | [mac-path]: http://stackoverflow.com/a/24322978/971592 94 | [issue]: https://github.com/epicweb-dev/get-started-with-react/issues/new 95 | 96 | -------------------------------------------------------------------------------- /public/og/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------