├── epicshop ├── .diffignore ├── .npmrc ├── in-browser-tests.spec.js ├── update-deps.sh ├── package.json ├── post-set-playground.js ├── Dockerfile ├── playwright.config.js ├── setup-custom.js ├── fly.yaml └── setup.js ├── .npmrc ├── public ├── favicon.ico ├── og │ └── background.png ├── react.js ├── images │ └── instructor.png ├── react-dom │ └── client.js └── favicon.svg ├── .prettierignore ├── .vscode └── extensions.json ├── exercises ├── FINISHED.mdx ├── 10.arrays │ ├── 03.solution.key-reset │ │ ├── index.test.ts │ │ ├── README.mdx │ │ └── index.tsx │ ├── FINISHED.mdx │ ├── 02.solution.focus-state │ │ ├── README.mdx │ │ └── key.test.ts │ ├── 03.problem.key-reset │ │ ├── index.tsx │ │ └── README.mdx │ ├── 02.problem.focus-state │ │ └── README.mdx │ ├── 01.solution.key-prop │ │ ├── key.test.ts │ │ ├── index.tsx │ │ └── README.mdx │ └── 01.problem.key-prop │ │ └── index.tsx ├── 07.forms │ ├── FINISHED.mdx │ ├── 04.solution.submit │ │ ├── api.server.ts │ │ ├── README.mdx │ │ ├── action.test.ts │ │ ├── api.test.ts │ │ ├── index.tsx │ │ └── submit.test.ts │ ├── 05.problem.action │ │ ├── api.server.ts │ │ ├── README.mdx │ │ └── index.tsx │ ├── 02.problem.action │ │ ├── api.server.ts │ │ ├── index.tsx │ │ └── README.mdx │ ├── 03.problem.types │ │ ├── api.server.ts │ │ ├── README.mdx │ │ └── index.tsx │ ├── 03.solution.types │ │ ├── api.server.ts │ │ ├── action.test.ts │ │ ├── README.mdx │ │ └── index.tsx │ ├── 05.solution.action │ │ ├── README.mdx │ │ └── index.tsx │ ├── 02.solution.action │ │ ├── api.server.ts │ │ ├── README.mdx │ │ ├── action.test.ts │ │ ├── index.tsx │ │ └── form.test.ts │ ├── 01.solution.form │ │ ├── README.mdx │ │ ├── index.tsx │ │ └── form.test.ts │ ├── 04.problem.submit │ │ ├── api.server.ts │ │ └── index.tsx │ └── 01.problem.form │ │ ├── README.mdx │ │ └── index.tsx ├── 08.inputs │ ├── 03.solution.radio │ │ └── README.mdx │ ├── 02.solution.select │ │ ├── README.mdx │ │ └── index.tsx │ ├── FINISHED.mdx │ ├── 05.solution.default-value │ │ └── README.mdx │ ├── 01.solution.checkbox │ │ ├── README.mdx │ │ └── index.tsx │ ├── 02.problem.select │ │ ├── README.mdx │ │ └── index.tsx │ ├── 04.problem.hidden │ │ └── README.mdx │ ├── 01.problem.checkbox │ │ ├── README.mdx │ │ └── index.tsx │ ├── 04.solution.hidden │ │ └── README.mdx │ ├── README.mdx │ └── 03.problem.radio │ │ ├── README.mdx │ │ └── index.tsx ├── 06.styling │ ├── 03.solution.size-prop │ │ ├── README.mdx │ │ ├── index.css │ │ ├── index.tsx │ │ └── boxes.test.ts │ ├── 01.solution.style │ │ ├── README.mdx │ │ ├── index.css │ │ ├── index.tsx │ │ └── boxes.test.ts │ ├── FINISHED.mdx │ ├── 02.problem.component │ │ ├── index.css │ │ ├── index.tsx │ │ └── README.mdx │ ├── 02.solution.component │ │ ├── index.css │ │ ├── README.mdx │ │ ├── index.tsx │ │ ├── boxes.test.ts │ │ └── box.test.tsx │ ├── 03.problem.size-prop │ │ ├── index.css │ │ ├── README.mdx │ │ └── index.tsx │ └── 01.problem.style │ │ ├── index.css │ │ ├── README.mdx │ │ └── index.tsx ├── 05.typescript │ ├── 03.solution.derive │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── 06.solution.satisfies │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── FINISHED.mdx │ ├── 04.solution.default-props │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── 05.solution.function-types │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── 01.solution.props │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── 02.solution.narrow │ │ ├── README.mdx │ │ ├── calculator.test.ts │ │ └── index.tsx │ ├── 01.problem.props │ │ ├── README.mdx │ │ └── index.tsx │ ├── 03.problem.derive │ │ ├── index.tsx │ │ └── README.mdx │ ├── 02.problem.narrow │ │ ├── index.tsx │ │ └── README.mdx │ ├── 06.problem.satisfies │ │ ├── index.tsx │ │ └── README.mdx │ ├── 04.problem.default-props │ │ ├── README.mdx │ │ └── index.tsx │ └── 05.problem.function-types │ │ ├── index.tsx │ │ └── README.mdx ├── 09.errors │ ├── FINISHED.mdx │ ├── 02.solution.show-boundary │ │ ├── README.mdx │ │ └── error-boundary.test.ts │ ├── 01.solution.composition │ │ ├── README.mdx │ │ └── error-boundary.test.ts │ ├── 03.problem.reset │ │ └── README.mdx │ ├── 03.solution.reset │ │ ├── README.mdx │ │ └── error-boundary.test.ts │ ├── 02.problem.show-boundary │ │ └── README.mdx │ └── 01.problem.composition │ │ └── README.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 │ │ ├── README.mdx │ │ └── index.test.ts │ ├── 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 ├── 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 ├── 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 │ ├── 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 ├── tsconfig.json ├── LICENSE.md ├── .gitignore ├── eslint.config.js ├── .github └── workflows │ └── validate.yml └── shared └── api-utils.ts /epicshop/.diffignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.* 3 | -------------------------------------------------------------------------------- /.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/react-fundamentals/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epicweb-dev/react-fundamentals/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/react-fundamentals/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/** -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "neotan.vscode-auto-restart-typescript-eslint-servers" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /exercises/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # React Fundamentals ⚛ 2 | 3 | 4 | 5 | Hooray! You're all done! 👏👏 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "extends": ["@epic-web/config/typescript"], 4 | "compilerOptions": { 5 | "paths": { 6 | "#*": ["./*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /exercises/10.arrays/03.solution.key-reset/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('this is just a demo', () => { 4 | expect(true).toBe(true) 5 | }) 6 | -------------------------------------------------------------------------------- /exercises/07.forms/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | 4 | 5 | 👨‍💼 Hooray! You've gotten a good start on how to use HTML forms with React! 6 | -------------------------------------------------------------------------------- /exercises/08.inputs/03.solution.radio/README.mdx: -------------------------------------------------------------------------------- 1 | # Radios 2 | 3 | 4 | 5 | 👨‍💼 Great! Now we have a nice way to select Public and Private. 6 | -------------------------------------------------------------------------------- /exercises/06.styling/03.solution.size-prop/README.mdx: -------------------------------------------------------------------------------- 1 | # Size Prop 2 | 3 | 4 | 5 | 👨‍💼 The constraints set people free! Great work on the `size` prop! 6 | -------------------------------------------------------------------------------- /exercises/05.typescript/03.solution.derive/README.mdx: -------------------------------------------------------------------------------- 1 | # Derive Types 2 | 3 | 4 | 5 | 👨‍💼 Great! That's definitely a win for our developer productivity! 6 | -------------------------------------------------------------------------------- /exercises/08.inputs/02.solution.select/README.mdx: -------------------------------------------------------------------------------- 1 | # Select 2 | 3 | 4 | 5 | 👨‍💼 Great work. Now our users can select an account type with some given options. 6 | -------------------------------------------------------------------------------- /exercises/08.inputs/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Inputs 2 | 3 | 4 | 5 | 👨‍💼 Great job! You've gotten a good start on how to use many of the input types 6 | in React. 7 | -------------------------------------------------------------------------------- /exercises/09.errors/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Error Boundaries 2 | 3 | 4 | 5 | 👨‍💼 Great work 👏 You now know how to manage errors in React components! 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 | -------------------------------------------------------------------------------- /exercises/05.typescript/06.solution.satisfies/README.mdx: -------------------------------------------------------------------------------- 1 | # Satisfies 2 | 3 | 4 | 5 | 👨‍💼 That is... 6 | 7 | ... 8 | 9 | ... 10 | 11 | Satisfactory. 🤡 12 | -------------------------------------------------------------------------------- /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/07.forms/04.solution.submit/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function action({ request }: { request: Request }) { 4 | const data = await request.formData() 5 | return respondWithDataTable(data) 6 | } 7 | -------------------------------------------------------------------------------- /exercises/07.forms/05.problem.action/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function action({ request }: { request: Request }) { 4 | const data = await request.formData() 5 | return respondWithDataTable(data) 6 | } 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/07.forms/02.problem.action/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function loader({ request }: { request: Request }) { 4 | const data = new URL(request.url).searchParams 5 | return respondWithDataTable(data) 6 | } 7 | -------------------------------------------------------------------------------- /exercises/07.forms/03.problem.types/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function loader({ request }: { request: Request }) { 4 | const data = new URL(request.url).searchParams 5 | return respondWithDataTable(data) 6 | } 7 | -------------------------------------------------------------------------------- /exercises/07.forms/03.solution.types/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function loader({ request }: { request: Request }) { 4 | const data = new URL(request.url).searchParams 5 | return respondWithDataTable(data) 6 | } 7 | -------------------------------------------------------------------------------- /exercises/07.forms/05.solution.action/README.mdx: -------------------------------------------------------------------------------- 1 | # Form Actions 2 | 3 | 4 | 5 | 👨‍💼 Nice! This is a handy feature from React for handling form submissions via 6 | the client. 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 | -------------------------------------------------------------------------------- /exercises/05.typescript/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | 4 | 5 | 👨‍💼 Awesome work. TypeScript is a must in this industry and using it with React 6 | is fairly straightforward. 7 | -------------------------------------------------------------------------------- /exercises/07.forms/02.solution.action/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | export async function loader({ request }: { request: Request }) { 4 | const data = new URL(request.url).searchParams 5 | return respondWithDataTable(data) 6 | } 7 | -------------------------------------------------------------------------------- /exercises/08.inputs/05.solution.default-value/README.mdx: -------------------------------------------------------------------------------- 1 | # Default Value 2 | 3 | 4 | 5 | 👨‍💼 Great! Now when the user loads up the form, they'll have a good default state 6 | to start from. 7 | -------------------------------------------------------------------------------- /exercises/07.forms/01.solution.form/README.mdx: -------------------------------------------------------------------------------- 1 | # Form 2 | 3 | 4 | 5 | 👨‍💼 Great job. We now have a form that we can type the username into and we can 6 | submit it. But we've got more work to do. Let's keep going. 7 | -------------------------------------------------------------------------------- /.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/05.typescript/04.solution.default-props/README.mdx: -------------------------------------------------------------------------------- 1 | # Default Props 2 | 3 | 4 | 5 | 👨‍💼 In the React world we use default props all the time, so I'm glad that you 6 | know all about how to do that now! 7 | -------------------------------------------------------------------------------- /exercises/06.styling/01.solution.style/README.mdx: -------------------------------------------------------------------------------- 1 | # Styling 2 | 3 | 4 | 5 | 👨‍💼 Great! We now know how to add a `className` and `style` prop to our elements. 6 | But we've got some repetition that would be nice to ditch... 7 | -------------------------------------------------------------------------------- /exercises/05.typescript/05.solution.function-types/README.mdx: -------------------------------------------------------------------------------- 1 | # Reduce Duplication 2 | 3 | 4 | 5 | 👨‍💼 Getting rid of duplication quite nice here. Well done! But we duplicated 6 | something else which is less nice... 7 | -------------------------------------------------------------------------------- /exercises/10.arrays/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Rendering Arrays 2 | 3 | 4 | 5 | 👨‍💼 Good work! You've successfully learned the importance of associating react 6 | elements to the data they represent when rendering arrays. 7 | -------------------------------------------------------------------------------- /exercises/05.typescript/01.solution.props/README.mdx: -------------------------------------------------------------------------------- 1 | # Props 2 | 3 | 4 | 5 | 👨‍💼 Great work! Now we'll get some type safety when people are using the 6 | `Calculator` component. But I think we can do better for the operator prop... 7 | -------------------------------------------------------------------------------- /exercises/09.errors/02.solution.show-boundary/README.mdx: -------------------------------------------------------------------------------- 1 | # Other Errors 2 | 3 | 4 | 5 | 👨‍💼 Great work! Now you can handle errors and give the user a better experience 6 | than nothing happening at all when they submit the form. 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /exercises/05.typescript/02.solution.narrow/README.mdx: -------------------------------------------------------------------------------- 1 | # Narrow Types 2 | 3 | 4 | 5 | 👨‍💼 Great, we've narrowed our types a bit which helps a lot with the experience 6 | of using our component props. But there's more to do! Let's keep going. 7 | -------------------------------------------------------------------------------- /exercises/07.forms/04.solution.submit/README.mdx: -------------------------------------------------------------------------------- 1 | # Submission 2 | 3 | 4 | 5 | 👨‍💼 Great work! There's a lot more to form submissions on the server side of 6 | things, but this will get us going to handling the UI side of this with React, 7 | so good job. 8 | -------------------------------------------------------------------------------- /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/06.styling/FINISHED.mdx: -------------------------------------------------------------------------------- 1 | # Styling 2 | 3 | 4 | 5 | 👨‍💼 Great work! Styling is all about the `style` and `className` props. There's 6 | much more to know about styling on the web, but that should get you going for 7 | 99% of your needs with React! Well done. 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 | -------------------------------------------------------------------------------- /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/07.forms/04.problem.submit/api.server.ts: -------------------------------------------------------------------------------- 1 | import { respondWithDataTable } from '#shared/api-utils' 2 | 3 | // 🐨 change this from `loader` to `action` 4 | export async function loader({ request }: { request: Request }) { 5 | // 🐨 change data to be `await request.formData()` 6 | const data = new URL(request.url).searchParams 7 | return respondWithDataTable(data) 8 | } 9 | -------------------------------------------------------------------------------- /exercises/07.forms/02.solution.action/README.mdx: -------------------------------------------------------------------------------- 1 | # Form Action 2 | 3 | 4 | 5 | 👨‍💼 Super! Now we're submitting the data to that endpoint. The default behavior 6 | of the form is to make a GET request which sends the data in the URL search 7 | params. We'll change it in the near future. 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 | -------------------------------------------------------------------------------- /exercises/06.styling/02.problem.component/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | .box--large { 9 | width: 270px; 10 | height: 270px; 11 | } 12 | .box--medium { 13 | width: 180px; 14 | height: 180px; 15 | } 16 | .box--small { 17 | width: 90px; 18 | height: 90px; 19 | } 20 | -------------------------------------------------------------------------------- /exercises/06.styling/02.solution.component/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | .box--large { 9 | width: 270px; 10 | height: 270px; 11 | } 12 | .box--medium { 13 | width: 180px; 14 | height: 180px; 15 | } 16 | .box--small { 17 | width: 90px; 18 | height: 90px; 19 | } 20 | -------------------------------------------------------------------------------- /exercises/06.styling/03.problem.size-prop/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | .box--large { 9 | width: 270px; 10 | height: 270px; 11 | } 12 | .box--medium { 13 | width: 180px; 14 | height: 180px; 15 | } 16 | .box--small { 17 | width: 90px; 18 | height: 90px; 19 | } 20 | -------------------------------------------------------------------------------- /exercises/06.styling/03.solution.size-prop/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | .box--large { 9 | width: 270px; 10 | height: 270px; 11 | } 12 | .box--medium { 13 | width: 180px; 14 | height: 180px; 15 | } 16 | .box--small { 17 | width: 90px; 18 | height: 90px; 19 | } 20 | -------------------------------------------------------------------------------- /exercises/10.arrays/03.solution.key-reset/README.mdx: -------------------------------------------------------------------------------- 1 | # Key Reset 2 | 3 | 4 | 5 | 🦉 This isn't something you're going to do a lot, but it's handy to know about 6 | for situations where you want to reset the state of a section of your React app. 7 | And hopefully it helps you understand keys a bit more as well. 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/06.styling/01.problem.style/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | 9 | .box--large { 10 | width: 270px; 11 | height: 270px; 12 | } 13 | 14 | .box--medium { 15 | width: 180px; 16 | height: 180px; 17 | } 18 | 19 | .box--small { 20 | width: 90px; 21 | height: 90px; 22 | } 23 | -------------------------------------------------------------------------------- /exercises/06.styling/01.solution.style/index.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | 9 | .box--large { 10 | width: 270px; 11 | height: 270px; 12 | } 13 | 14 | .box--medium { 15 | width: 180px; 16 | height: 180px; 17 | } 18 | 19 | .box--small { 20 | width: 90px; 21 | height: 90px; 22 | } 23 | -------------------------------------------------------------------------------- /exercises/09.errors/01.solution.composition/README.mdx: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | 4 | 5 | 👨‍💼 Great job! You've successfully wrapped our app logic in an error boundary 6 | that can handle any errors that occur in the app. But what about other kinds of 7 | errors? The kind that React doesn't know about? Let's look at that next. 8 | -------------------------------------------------------------------------------- /exercises/01.js-hello-world/01.solution.hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /exercises/08.inputs/01.solution.checkbox/README.mdx: -------------------------------------------------------------------------------- 1 | # Checkbox 2 | 3 | 4 | 5 | 👨‍💼 Great job! Interesting (weird) how checkboxes work huh? 6 | 7 | You may have also noticed that the `age` input is a string even though its 8 | `type` attribute is `number`. It's good to understand how values are submitted 9 | for different input types! 10 | -------------------------------------------------------------------------------- /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.3", 9 | "@epic-web/workshop-app": "^6.47.1", 10 | "@epic-web/workshop-utils": "^6.47.1", 11 | "epicshop": "^6.47.1", 12 | "execa": "^9.6.0", 13 | "fs-extra": "^11.3.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /exercises/07.forms/02.solution.action/action.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | import './index.tsx' 4 | 5 | const form = await dtl.waitFor(() => { 6 | const form = document.querySelector('form') 7 | expect(form).toBeInTheDocument() 8 | return form 9 | }) 10 | 11 | await testStep('Form has correct action', async () => { 12 | expect(form).toHaveAttribute('action', 'api/onboarding') 13 | }) 14 | -------------------------------------------------------------------------------- /exercises/07.forms/03.solution.types/action.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | import './index.tsx' 4 | 5 | const form = await dtl.waitFor(() => { 6 | const form = document.querySelector('form') 7 | expect(form).toBeInTheDocument() 8 | return form 9 | }) 10 | 11 | await testStep('Form has correct action', async () => { 12 | expect(form).toHaveAttribute('action', 'api/onboarding') 13 | }) 14 | -------------------------------------------------------------------------------- /exercises/06.styling/02.solution.component/README.mdx: -------------------------------------------------------------------------------- 1 | # Custom Component 2 | 3 | 4 | 5 | 👨‍💼 Great work with the `` component. But we can make this experience even 6 | better by designing our box around the different sizes. Let's keep going! 7 | 8 | 🚨 Also, don't forget to export your Box component (tests will need it!) `export function Box...` 9 | -------------------------------------------------------------------------------- /exercises/07.forms/04.solution.submit/action.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | import './index.tsx' 4 | 5 | const form = await dtl.waitFor(() => { 6 | const form = document.querySelector('form') 7 | expect(form).toBeInTheDocument() 8 | return form 9 | }) 10 | 11 | await testStep('Form does not have incorrect method', async () => { 12 | expect(form).not.toHaveAttribute('method', 'GET') 13 | }) 14 | -------------------------------------------------------------------------------- /exercises/10.arrays/02.solution.focus-state/README.mdx: -------------------------------------------------------------------------------- 1 | # Focus State 2 | 3 | 4 | 5 | 🦉 There are some other interesting things you can do with `key`s as well (like 6 | changing them on an element to intentionally reset the state of a component). 7 | Feel free to play around with that if you like, but we'll be using that in a 8 | future workshop so look forward to it! 9 | -------------------------------------------------------------------------------- /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/08.inputs/02.problem.select/README.mdx: -------------------------------------------------------------------------------- 1 | # Select 2 | 3 | 4 | 5 | 👨‍💼 We have four different account types: 6 | 7 | - Admin 8 | - Teacher 9 | - Parent 10 | - Student 11 | 12 | Please add a `` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select). 16 | -------------------------------------------------------------------------------- /exercises/10.arrays/03.solution.key-reset/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | function App() { 5 | const [key, setKey] = useState(0) 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | const rootEl = document.createElement('div') 15 | document.body.append(rootEl) 16 | createRoot(rootEl).render() 17 | -------------------------------------------------------------------------------- /exercises/07.forms/01.solution.form/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | ) 13 | } 14 | 15 | const rootEl = document.createElement('div') 16 | document.body.append(rootEl) 17 | createRoot(rootEl).render() 18 | -------------------------------------------------------------------------------- /exercises/07.forms/04.solution.submit/api.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep } from '@epic-web/workshop-utils/test' 2 | 3 | await testStep('POST api/onboarding', async () => { 4 | const formData = new FormData() 5 | const name = 'Kody Koala' 6 | formData.set('name', name) 7 | const response = await fetch('api/onboarding', { 8 | method: 'POST', 9 | body: formData, 10 | }) 11 | 12 | expect(response.status).toBe(200) 13 | expect(await response.text()).toContain(name) 14 | }) 15 | -------------------------------------------------------------------------------- /epicshop/post-set-playground.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | const exclude = [ 5 | 'exercises/01.', 6 | 'exercises/02.', 7 | 'exercises/03.', 8 | 'exercises/04.', 9 | ] 10 | 11 | if ( 12 | exclude.every((e) => !process.env.EPICSHOP_PLAYGROUND_SRC_DIR.includes(e)) 13 | ) { 14 | fs.writeFileSync( 15 | path.join(process.env.EPICSHOP_PLAYGROUND_DEST_DIR, 'tsconfig.json'), 16 | JSON.stringify({ extends: '../tsconfig' }, null, 2), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /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/07.forms/01.problem.form/README.mdx: -------------------------------------------------------------------------------- 1 | # Form 2 | 3 | 4 | 5 | 👨‍💼 We want to have an intake form which accepts a number of bits of information 6 | for a new user. They'll need a username to start with, so let's just render that 7 | and the submit button for now. 8 | 9 | 10 | 🦉 You may notice a full page refresh when you submit the form. We'll talk 11 | about this in a future step. 12 | 13 | -------------------------------------------------------------------------------- /exercises/07.forms/02.solution.action/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | ) 13 | } 14 | 15 | const rootEl = document.createElement('div') 16 | document.body.append(rootEl) 17 | createRoot(rootEl).render() 18 | -------------------------------------------------------------------------------- /exercises/07.forms/02.problem.action/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | return ( 5 | // 🐨 add an action prop pointing to "api/onboarding" 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | const rootEl = document.createElement('div') 17 | document.body.append(rootEl) 18 | createRoot(rootEl).render() 19 | -------------------------------------------------------------------------------- /exercises/10.arrays/03.problem.key-reset/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | function App() { 5 | const [key, setKey] = useState(0) 6 | return ( 7 |
8 | {/* 🐨 add a key prop to this input and set it to the key state */} 9 | 10 | 11 |
12 | ) 13 | } 14 | 15 | const rootEl = document.createElement('div') 16 | document.body.append(rootEl) 17 | createRoot(rootEl).render() 18 | -------------------------------------------------------------------------------- /exercises/07.forms/01.problem.form/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | // 🐨 render a form 5 | // 🐨 render a "Username" label 6 | // 🐨 render an input with the name "username" 7 | // 🐨 render a button 8 | // 💯 associate the label to the input using htmlFor and id attributes 9 | // 💯 explicitly set the button type appropriately 10 | return 'TODO' 11 | } 12 | 13 | const rootEl = document.createElement('div') 14 | document.body.append(rootEl) 15 | createRoot(rootEl).render() 16 | -------------------------------------------------------------------------------- /exercises/08.inputs/04.problem.hidden/README.mdx: -------------------------------------------------------------------------------- 1 | # Hidden Inputs 2 | 3 | 4 | 5 | 👨‍💼 Our backend needs to know what organization to associate this new account to. 6 | We don't need to ask the user this information because we know that it's just 7 | `123`. So we can add 8 | [a hidden input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden) 9 | to make sure the submission includes this value, but not require the user to 10 | provide it. 11 | -------------------------------------------------------------------------------- /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 6 | get us a centimeter closer to our goal of using JSX with custom components. 7 | It's 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(' 4 | 5 | 👨‍💼 Our developers are having a hard time using the `Calculator` component 6 | properly, so I want you to add type annotations. 7 | 8 | I'd also like you to meet Lily the Life Jacket! 🦺 She's going to be hanging 9 | around with the rest of the crew to indicate wherever there's something that's 10 | TypeScript-specific you need to do and to give you TypeScript-specific tips. 11 | You'll be working with Lily the Life Jacket a lot in this exercise. 12 | 13 | Follow the emoji! 14 | -------------------------------------------------------------------------------- /exercises/10.arrays/02.problem.focus-state/README.mdx: -------------------------------------------------------------------------------- 1 | # Focus State 2 | 3 | 4 | 5 | 🦉 You can observe that when we're talking about "state" we're also talking 6 | about keyboard focus as well as what text is selected! As you play around with 7 | this, try selecting text in the inputs and observe how the first two examples 8 | differ from the last one. You'll notice that using the array `index` as a key is 9 | no different from React's default behavior, so it's unlikely to fix issues if 10 | you're having them. Best to use a unique ID. Play around with it! 11 | -------------------------------------------------------------------------------- /exercises/09.errors/03.problem.reset/README.mdx: -------------------------------------------------------------------------------- 1 | # Reset 2 | 3 | 4 | 5 | 👨‍💼 Sometimes errors are temporary and will go away if you just "turn it off and 6 | on again." `react-error-boundary` supports this with a `resetErrorBoundary` 7 | function it passes to our `FallbackComponent`. We can use this to reset the 8 | error boundary and try again. 9 | 10 | Once you're done, you can test it out. We've still got the typo in our 11 | `onSubmit` handler, so go ahead and trigger the error by submitting the form, 12 | then click the "Try again" button to see the error go away. 13 | -------------------------------------------------------------------------------- /exercises/09.errors/01.solution.composition/error-boundary.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep( 7 | 'Error boundary is rendered immediately due to render error', 8 | async () => { 9 | // Check if the error message is displayed 10 | const errorMessage = await screen.findByRole('alert') 11 | expect(errorMessage).toBeDefined() 12 | expect(errorMessage.textContent).toContain('There was an error:') 13 | 14 | // Ensure the form is not rendered 15 | const form = screen.queryByRole('form') 16 | expect(form).toBeNull() 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /exercises/02.raw-react/01.solution.elements/index.html: -------------------------------------------------------------------------------- 1 | 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/10.arrays/03.problem.key-reset/README.mdx: -------------------------------------------------------------------------------- 1 | # Key Reset 2 | 3 | 4 | 5 | 🦉 Interestingly, we can use the `key` as a mechanism for removing and re-adding 6 | elements to the DOM. This is useful when we want to reset the state of a 7 | component or elements within a component. 8 | 9 | We haven't covered state yet, but state is necessary for this exercise so that's 10 | done for you. All we need you to do is add the key prop and see how that affects 11 | the state of the app. Enjoy! 12 | 13 | 👨‍💼 Type in the input, then click "reset" and think about why that works the way 14 | it does. 15 | -------------------------------------------------------------------------------- /exercises/09.errors/03.solution.reset/README.mdx: -------------------------------------------------------------------------------- 1 | # Reset 2 | 3 | 4 | 5 | 👨‍💼 Great! It's nice to give our users a path forward. One thing you may have 6 | noticed is that if you enter in values into the form and there's an error, when 7 | you restore your form, the values are gone. This is because we're not 8 | persisting the values in the form. That's beyond the scope of this exercise, but 9 | it's something to keep in mind. If that's something important to your situation, 10 | you'll need to find a way to persist the form values and restore those values 11 | when the form is rendered again. 12 | -------------------------------------------------------------------------------- /exercises/10.arrays/02.solution.focus-state/key.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | const originalError = console.error 4 | const errors: Array = [] 5 | console.error = (...args: any[]) => { 6 | errors.push(args[0]) 7 | originalError(...args) 8 | } 9 | 10 | import './index.tsx' 11 | 12 | const { screen } = dtl 13 | 14 | await testStep('Wait for things to render', async () => { 15 | await screen.findByText('Without a key') 16 | }) 17 | 18 | await testStep('Key prop is provided (no console errors)', async () => { 19 | try { 20 | expect(errors).toHaveLength(0) 21 | } finally { 22 | console.error = originalError 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /exercises/03.using-jsx/01.solution.compiler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /exercises/07.forms/03.problem.types/README.mdx: -------------------------------------------------------------------------------- 1 | # Input Types 2 | 3 | 4 | 5 | 👨‍💼 We need quite a few more fields. Luckily the web platform has some great 6 | input types we can use. 📜 Check out all the 7 | [standard `` types here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#input_types). 8 | 9 | For us, we need to add types for the password, photo, favorite color, and start 10 | date. Please add inputs for those fields. 11 | 12 | 13 | 🦉 You may notice a full page refresh when you submit the form. We'll talk 14 | about this in a future step. 15 | 16 | -------------------------------------------------------------------------------- /exercises/06.styling/01.problem.style/README.mdx: -------------------------------------------------------------------------------- 1 | # Styling 2 | 3 | 4 | 5 | 👨‍💼 We have a CSS file that's being loaded on the 6 | page using a link tag: 7 | 8 | ```html 9 | 10 | ``` 11 | 12 | In our isolated learning environment, the workshop app gets the styles on the 13 | page for us automatically. In a real-world app, you'll need to add the `` 14 | tag yourself (or use a tool which does this for you). 15 | 16 | Your job is to apply the right `className` and `style` props to the divs so the 17 | styles applied match the text content. 18 | -------------------------------------------------------------------------------- /exercises/10.arrays/01.solution.key-prop/key.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | 3 | const originalError = console.error 4 | const errors: Array = [] 5 | console.error = (...args: any[]) => { 6 | errors.push(args[0]) 7 | originalError(...args) 8 | } 9 | 10 | import './index.tsx' 11 | 12 | const { screen } = dtl 13 | 14 | await testStep('Wait for Add Item button', async () => { 15 | await screen.findByRole('button', { name: /add item/i }) 16 | }) 17 | 18 | await testStep('Key prop is provided (no console errors)', async () => { 19 | try { 20 | expect(errors).toHaveLength(0) 21 | } finally { 22 | console.error = originalError 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /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/07.forms/01.solution.form/form.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Form is rendered', () => { 7 | return dtl.waitFor(() => { 8 | const form = document.querySelector('form') 9 | expect(form).toBeInTheDocument() 10 | return form 11 | }) 12 | }) 13 | 14 | await testStep('Username input is rendered', async () => { 15 | const usernameInput = await screen.findByLabelText(/username/i) 16 | expect(usernameInput).toHaveAttribute('name', 'username') 17 | }) 18 | 19 | await testStep('Submit button is rendered', () => 20 | screen.findByRole('button', { name: /submit/i }), 21 | ) 22 | -------------------------------------------------------------------------------- /exercises/07.forms/02.solution.action/form.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Form is rendered', () => { 7 | return dtl.waitFor(() => { 8 | const form = document.querySelector('form') 9 | expect(form).toBeInTheDocument() 10 | return form 11 | }) 12 | }) 13 | 14 | await testStep('Username input is rendered', async () => { 15 | const usernameInput = await screen.findByLabelText(/username/i) 16 | expect(usernameInput).toHaveAttribute('name', 'username') 17 | }) 18 | 19 | await testStep('Submit button is rendered', () => 20 | screen.findByRole('button', { name: /submit/i }), 21 | ) 22 | -------------------------------------------------------------------------------- /exercises/07.forms/03.problem.types/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | {/* 🐨 add appropriate inputs for: 11 | - password 12 | - age (accepting numbers from 0 to 200) 13 | - photo (restricts the input to only accept image files) 14 | - color 15 | - startDate 16 | */} 17 | 18 |
19 | ) 20 | } 21 | 22 | const rootEl = document.createElement('div') 23 | document.body.append(rootEl) 24 | createRoot(rootEl).render() 25 | -------------------------------------------------------------------------------- /exercises/08.inputs/01.problem.checkbox/README.mdx: -------------------------------------------------------------------------------- 1 | # Checkbox 2 | 3 | 4 | 5 | 👨‍💼 We need to add a checkbox for whether the user has already signed a waiver. 6 | 7 | For checkboxes, typically they appear on the left side of the label and in this 8 | case we can even have the checkbox be _inside_ the label which means we don't 9 | need to worry about the `for` and `id` attributes which is nice. 10 | 11 | Please add the checkbox to the form. 12 | 13 | Once you've done that, make sure to submit the form with the checkbox checked 14 | and without it checked so you can familiarize yourself with the difference in 15 | form data because it's kinda funny. 16 | -------------------------------------------------------------------------------- /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/react-fundamentals 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/07.forms/05.problem.action/README.mdx: -------------------------------------------------------------------------------- 1 | # Form Actions 2 | 3 | 4 | 5 | 👨‍💼 `onSubmit` handlers that prevent default and handle file uploads like this 6 | are so common that React has a built-in way to do that! The `action` prop on a 7 | form can accept a function! The function accepts a `formData` object. 8 | 9 | 10 | Note that in HTML, attributes cannot accept functions. The action prop 11 | accepting a function is a React feature. 12 | 13 | 14 | In this exercise, create a function for logging the `formData` like we are 15 | currently and delete all the superfluous props. 16 | 17 | 📜 [`
` React docs](https://react.dev/reference/react-dom/components/form) 18 | -------------------------------------------------------------------------------- /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/09.errors/02.solution.show-boundary/error-boundary.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | import { userEvent } from '@testing-library/user-event' 4 | 5 | const user = userEvent.setup() 6 | 7 | import './index.tsx' 8 | 9 | await testStep('Error boundary is rendered after form submission', async () => { 10 | // submit the form 11 | await user.click(await screen.findByRole('button', { name: /submit/i })) 12 | 13 | // Check if the error message is displayed 14 | const errorMessage = await screen.findByRole('alert') 15 | expect(errorMessage).toBeDefined() 16 | expect(errorMessage.textContent).toContain('There was an error:') 17 | 18 | // Ensure the form is not rendered after error 19 | const form = screen.queryByRole('form') 20 | expect(form).toBeNull() 21 | }) 22 | -------------------------------------------------------------------------------- /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/06.styling/03.problem.size-prop/README.mdx: -------------------------------------------------------------------------------- 1 | # Size Prop 2 | 3 | 4 | 5 | 👨‍💼 It's great that we're composing the `className`s and `style`s properly, but 6 | wouldn't it be better if the users of our components didn't have to worry about 7 | which class name to apply for a given effect? Or that a class name is involved 8 | at all? I think it would be better if users of our component had a `size` prop 9 | and our component took care of making the box that size. 10 | 11 | In this step, try to make this API work: 12 | 13 | ```tsx 14 | 15 | small lightblue box 16 | 17 | ``` 18 | 19 | - You'll need 📜 20 | [TypeScript's intersection (&) operator](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) 21 | -------------------------------------------------------------------------------- /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/07.forms/03.solution.types/README.mdx: -------------------------------------------------------------------------------- 1 | # Input Types 2 | 3 | 4 | 5 | 👨‍💼 Great! Now our users will have more fields available to them as they're 6 | filling out this form. But there's something weird going on with the form 7 | submission so let's look at that next. 8 | 9 | 🦉 While the web platform has a lot of really great input types, not all of them 10 | match the style you're going for in your app. And these can be difficult (or in 11 | some cases even impossible) to style. There are efforts in standards bodies to 12 | solve these problems, but until then, you should look into great UI libraries 13 | like [Radix-UI](https://www.radix-ui.com/) and 14 | [React Aria Components](https://react-spectrum.adobe.com/react-aria) which 15 | handle these issues and allow you to customize the style much more easily. 16 | -------------------------------------------------------------------------------- /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/06.styling/02.solution.component/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | export function Box({ 4 | style = {}, 5 | className = '', 6 | ...otherProps 7 | }: React.ComponentProps<'div'>) { 8 | return ( 9 |
14 | ) 15 | } 16 | 17 | function App() { 18 | return ( 19 |
20 | 21 | small lightblue box 22 | 23 | 24 | medium pink box 25 | 26 | 27 | large orange box 28 | 29 | sizeless colorless box 30 |
31 | ) 32 | } 33 | 34 | const rootEl = document.createElement('div') 35 | document.body.append(rootEl) 36 | createRoot(rootEl).render() 37 | -------------------------------------------------------------------------------- /exercises/08.inputs/04.solution.hidden/README.mdx: -------------------------------------------------------------------------------- 1 | # Hidden Inputs 2 | 3 | 4 | 5 | 👨‍💼 Great! Now when the user submits the form, this extra information will be 6 | available in the submission without the user needing to supply it. Well done! 7 | 8 | 🦉 A common question here is whether you can supply complex data structures as 9 | hidden input values, and you definitely can. You can either provide multiple 10 | hidden inputs, or just stick JSON in your hidden input like so: 11 | 12 | ```tsx 13 | 18 | ``` 19 | 20 | Then the backend could parse that JSON from the `order` form field. This can be 21 | very useful if you're using a library that doesn't render form elements, but you 22 | want to send the user's selections to the backend as a part of your form using 23 | your own form elements. 24 | -------------------------------------------------------------------------------- /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( 24 | inlineScript.textContent, 25 | 'expected script to include { children }', 26 | ).to.match(/{\s*children\s*}/) 27 | }) 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | outputDir: path.join(tmpDir, 'playwright-test-output'), 14 | reporter: [ 15 | [ 16 | 'html', 17 | { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, 18 | ], 19 | ], 20 | use: { 21 | baseURL: `http://localhost:${PORT}/`, 22 | trace: 'retain-on-failure', 23 | }, 24 | 25 | projects: [ 26 | { 27 | name: 'chromium', 28 | use: { ...devices['Desktop Chrome'] }, 29 | }, 30 | ], 31 | 32 | webServer: { 33 | command: 'cd .. && npm start', 34 | port: Number(PORT), 35 | reuseExistingServer: !process.env.CI, 36 | stdout: 'pipe', 37 | stderr: 'pipe', 38 | env: { PORT }, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /exercises/04.components/01.solution.function/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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?.toLowerCase()).to.equal('hello world') 25 | expect(goodbyeMessage?.textContent?.toLowerCase()).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?.toLowerCase()).to.equal('hello world') 25 | expect(goodbyeMessage?.textContent?.toLowerCase()).to.equal('goodbye world') 26 | }) 27 | -------------------------------------------------------------------------------- /exercises/02.raw-react/02.problem.nesting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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?.toLowerCase()).to.equal('hello world') 25 | expect(goodbyeMessage?.textContent?.toLowerCase()).to.equal('goodbye world') 26 | }) 27 | -------------------------------------------------------------------------------- /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/08.inputs/README.mdx: -------------------------------------------------------------------------------- 1 | # Inputs 2 | 3 | 4 | 5 | There are lots of different types of form elements built-into the browser you 6 | can use (of varying degrees of usefulness because of the way they are designed). 7 | [Find a list of standard input types here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input). 8 | 9 | In general, you use these form elements in React the same way you use them in 10 | regular HTML, but there are a few exceptions (especially regarding default 11 | values). So we're going to explore all the types of form elements in this 12 | exercise. 13 | 14 | There are a number of ways you can be notified of changes to form elements. 15 | We'll not be covering them in this workshop (we'll get into it more in a future 16 | workshop when we talk about managing state), but if you just can't wait, play 17 | around with `onChange` (which behaves like 18 | [the browser's `input` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event)). 19 | -------------------------------------------------------------------------------- /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/06.styling/01.solution.style/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | const smallBox = ( 4 |
8 | small lightblue box 9 |
10 | ) 11 | const mediumBox = ( 12 |
16 | medium pink box 17 |
18 | ) 19 | const largeBox = ( 20 |
24 | large orange box 25 |
26 | ) 27 | const sizelessColorlessBox = ( 28 |
29 | sizeless colorless box 30 |
31 | ) 32 | 33 | function App() { 34 | return ( 35 |
36 | {smallBox} 37 | {mediumBox} 38 | {largeBox} 39 | {sizelessColorlessBox} 40 |
41 | ) 42 | } 43 | 44 | const rootEl = document.createElement('div') 45 | document.body.append(rootEl) 46 | createRoot(rootEl).render() 47 | -------------------------------------------------------------------------------- /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/06.styling/03.solution.size-prop/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | export function Box({ 4 | size, 5 | ...otherProps 6 | }: { 7 | size?: 'small' | 'medium' | 'large' 8 | } & React.ComponentProps<'div'>) { 9 | const sizeClassName = size ? `box--${size}` : '' 10 | return ( 11 |
18 | ) 19 | } 20 | 21 | function App() { 22 | return ( 23 |
24 | 25 | small lightblue box 26 | 27 | 28 | medium pink box 29 | 30 | 31 | large orange box 32 | 33 | sizeless colorless box 34 |
35 | ) 36 | } 37 | 38 | const rootEl = document.createElement('div') 39 | document.body.append(rootEl) 40 | createRoot(rootEl).render() 41 | -------------------------------------------------------------------------------- /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-react-fundamentals' 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/05.typescript/01.solution.props/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('1 - 2 = -1') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 2 = 2') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('1 / 2 = 0.5') 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/05.typescript/02.solution.narrow/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('1 - 2 = -1') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 2 = 2') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('1 / 2 = 0.5') 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/05.typescript/03.solution.derive/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('1 - 2 = -1') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 2 = 2') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('1 / 2 = 0.5') 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/05.typescript/06.solution.satisfies/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('0 - 0 = 0') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 0 = 0') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('0 / 2 = 0') 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/05.typescript/04.solution.default-props/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('0 - 0 = 0') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 0 = 0') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('0 / 2 = 0') 32 | }) 33 | -------------------------------------------------------------------------------- /exercises/05.typescript/05.solution.function-types/calculator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | 4 | import './index.tsx' 5 | 6 | await testStep('Calculator h1 is rendered', () => 7 | screen.findByText('Calculator'), 8 | ) 9 | 10 | const calculators = await testStep('Code elements are rendered', async () => { 11 | const elements = await screen.findAllByRole('code') 12 | expect(elements).toHaveLength(4) 13 | return elements 14 | }) 15 | 16 | const [add, subtract, multiply, divide] = calculators 17 | 18 | await testStep('Addition calculation is rendered correctly', async () => { 19 | expect(add).toHaveTextContent('1 + 2 = 3') 20 | }) 21 | 22 | await testStep('Subtraction calculation is rendered correctly', async () => { 23 | expect(subtract).toHaveTextContent('0 - 0 = 0') 24 | }) 25 | 26 | await testStep('Multiplication calculation is rendered correctly', async () => { 27 | expect(multiply).toHaveTextContent('1 * 0 = 0') 28 | }) 29 | 30 | await testStep('Division calculation is rendered correctly', async () => { 31 | expect(divide).toHaveTextContent('0 / 2 = 0') 32 | }) 33 | -------------------------------------------------------------------------------- /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/05.typescript/04.solution.default-props/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | const operations = { 4 | '+': (left: number, right: number): number => left + right, 5 | '-': (left: number, right: number): number => left - right, 6 | '*': (left: number, right: number): number => left * right, 7 | '/': (left: number, right: number): number => left / right, 8 | } 9 | 10 | type CalculatorProps = { 11 | left?: number 12 | operator?: keyof typeof operations 13 | right?: number 14 | } 15 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) { 16 | const result = operations[operator](left, right) 17 | return ( 18 |
19 | 20 | {left} {operator} {right} = {result} 21 | 22 |
23 | ) 24 | } 25 | 26 | function App() { 27 | return ( 28 |
29 |

Calculator

30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | 38 | const rootEl = document.createElement('div') 39 | document.body.append(rootEl) 40 | createRoot(rootEl).render() 41 | -------------------------------------------------------------------------------- /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/07.forms/02.problem.action/README.mdx: -------------------------------------------------------------------------------- 1 | # Form Action 2 | 3 | 4 | 5 | 🧝‍♂️ I've added some backend code that will handle our form submission. It's at 6 | `api/onboarding`. You can find it in . 7 | 8 | 9 | Note: the `api.server.ts` file is some magic from the Workshop app to make it 10 | so we can test things out with a backend server without having to actually set 11 | up a backend server. It's not something you'd use in a real app. 12 | 13 | 14 | 👨‍💼 Great! So we want the user's form submission to be sent along to 15 | `api/onboarding`. By default, when a form is submitted, the browser will send 16 | the form data to the current page's URL. We can change this by adding an 17 | `action` attribute to the form element: 18 | 19 | ```html 20 | 21 | 22 | 23 | ``` 24 | 25 | Go ahead and add an `action` attribute to the form element. 26 | 27 | 28 | 🦉 You may notice a full page refresh when you submit the form. We'll talk 29 | about this in a future step. 30 | 31 | -------------------------------------------------------------------------------- /exercises/05.typescript/06.solution.satisfies/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | type OperationFn = (left: number, right: number) => number 4 | 5 | const operations = { 6 | '+': (left, right) => left + right, 7 | '-': (left, right) => left - right, 8 | '*': (left, right) => left * right, 9 | '/': (left, right) => left / right, 10 | } satisfies Record 11 | 12 | type CalculatorProps = { 13 | left?: number 14 | operator?: keyof typeof operations 15 | right?: number 16 | } 17 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) { 18 | const result = operations[operator](left, right) 19 | return ( 20 |
21 | 22 | {left} {operator} {right} = {result} 23 | 24 |
25 | ) 26 | } 27 | 28 | function App() { 29 | return ( 30 |
31 |

Calculator

32 | 33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | 40 | const rootEl = document.createElement('div') 41 | document.body.append(rootEl) 42 | createRoot(rootEl).render() 43 | -------------------------------------------------------------------------------- /exercises/06.styling/01.problem.style/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | // 🐨 add a className prop to each div and apply the correct class names 4 | // based on the text content 5 | // 💰 Here are the available class names: box, box--large, box--medium, box--small 6 | // 💰 each of the elements should have the "box" className applied 7 | 8 | // 🐨 add a style prop to each div so their background color 9 | // matches what the text says it should be 10 | // 🐨 also use the style prop to make the font italic 11 | // 💰 Here are available style attributes: backgroundColor, fontStyle 12 | 13 | const smallBox =
small lightblue box
14 | const mediumBox =
medium pink box
15 | const largeBox =
large orange box
16 | 17 | // 💰 the sizelessColorlessBox should still be a box, just with no size or color 18 | const sizelessColorlessBox =
sizeless colorless box
19 | 20 | function App() { 21 | return ( 22 |
23 | {smallBox} 24 | {mediumBox} 25 | {largeBox} 26 | {sizelessColorlessBox} 27 |
28 | ) 29 | } 30 | 31 | const rootEl = document.createElement('div') 32 | document.body.append(rootEl) 33 | createRoot(rootEl).render() 34 | -------------------------------------------------------------------------------- /exercises/09.errors/02.problem.show-boundary/README.mdx: -------------------------------------------------------------------------------- 1 | # Other Errors 2 | 3 | 4 | 5 | 👨‍💼 Recall previously we learned that React doesn't catch all errors for you. 6 | We've got an error in our `onSubmit` handler like this. If you try to submit the 7 | form, you'll notice in the console that there's an error (someone made a typo 8 | 😅). The error looks something like: 9 | 10 | ```javascript 11 | Uncaught TypeError: Cannot read properties of null (reading 'toUpperCase') 12 | ``` 13 | 14 | Before we fix the typo, we want to give the user better feedback for when 15 | something like this happens (right now, they don't know it's not working and 16 | that's terribly annoying). So what I want you to do is wrap that callback in our 17 | own `try/catch` and then surface it to React using the `showBoundary` function 18 | you get when you call the `useErrorBoundary` utility: 19 | 20 | ```tsx 21 | import { useErrorBoundary } from 'react-error-boundary' 22 | 23 | // ... in the component 24 | const { showBoundary } = useErrorBoundary() 25 | 26 | // ... in the `catch` block 27 | showBoundary(error) 28 | ``` 29 | 30 | So let's make that happen! 31 | -------------------------------------------------------------------------------- /exercises/05.typescript/02.solution.narrow/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | const operations = { 4 | '+': (left: number, right: number): number => left + right, 5 | '-': (left: number, right: number): number => left - right, 6 | '*': (left: number, right: number): number => left * right, 7 | '/': (left: number, right: number): number => left / right, 8 | } 9 | 10 | type CalculatorProps = { 11 | left: number 12 | operator: '+' | '-' | '*' | '/' 13 | right: number 14 | } 15 | function Calculator({ left, operator, right }: CalculatorProps) { 16 | const result = operations[operator](left, right) 17 | return ( 18 |
19 | 20 | {left} {operator} {right} = {result} 21 | 22 |
23 | ) 24 | } 25 | 26 | function App() { 27 | return ( 28 |
29 |

Calculator

30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | 38 | const rootEl = document.createElement('div') 39 | document.body.append(rootEl) 40 | createRoot(rootEl).render() 41 | -------------------------------------------------------------------------------- /exercises/05.typescript/03.solution.derive/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | const operations = { 4 | '+': (left: number, right: number): number => left + right, 5 | '-': (left: number, right: number): number => left - right, 6 | '*': (left: number, right: number): number => left * right, 7 | '/': (left: number, right: number): number => left / right, 8 | } 9 | 10 | type CalculatorProps = { 11 | left: number 12 | operator: keyof typeof operations 13 | right: number 14 | } 15 | function Calculator({ left, operator, right }: CalculatorProps) { 16 | const result = operations[operator](left, right) 17 | return ( 18 |
19 | 20 | {left} {operator} {right} = {result} 21 | 22 |
23 | ) 24 | } 25 | 26 | function App() { 27 | return ( 28 |
29 |

Calculator

30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | 38 | const rootEl = document.createElement('div') 39 | document.body.append(rootEl) 40 | createRoot(rootEl).render() 41 | -------------------------------------------------------------------------------- /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/05.typescript/05.solution.function-types/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | type OperationFn = (left: number, right: number) => number 4 | type Operator = '+' | '-' | '/' | '*' 5 | const operations: Record = { 6 | '+': (left, right) => left + right, 7 | '-': (left, right) => left - right, 8 | '*': (left, right) => left * right, 9 | '/': (left, right) => left / right, 10 | } 11 | 12 | type CalculatorProps = { 13 | left?: number 14 | operator?: keyof typeof operations 15 | right?: number 16 | } 17 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) { 18 | const result = operations[operator](left, right) 19 | return ( 20 |
21 | 22 | {left} {operator} {right} = {result} 23 | 24 |
25 | ) 26 | } 27 | 28 | function App() { 29 | return ( 30 |
31 |

Calculator

32 | 33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | 40 | const rootEl = document.createElement('div') 41 | document.body.append(rootEl) 42 | createRoot(rootEl).render() 43 | -------------------------------------------------------------------------------- /exercises/09.errors/01.problem.composition/README.mdx: -------------------------------------------------------------------------------- 1 | # Composition 2 | 3 | 4 | 5 | 👨‍💼 You'll notice that right now our onboarding form isn't showing up at all. 6 | That's because someone made a typo... But errors like this could happen for any 7 | number of reasons (data issue, API version change, network error, etc.). So we 8 | should definitely handle errors in our app. 9 | 10 | Let's start by wrapping our existing `App` component in an Error Boundary to 11 | handle any errors. We're going to have you use the `FallbackComponent` prop on 12 | the `ErrorBoundary` so we can define our own `ErrorFallback` component. 13 | 14 | ```tsx 15 | {/*...*/} 16 | ``` 17 | 18 | Right now we have an `App` component which we render using 19 | `createRoot(rootEl).render()`, so to get our stuff wrapped 20 | properly, we'll rename our existing `App` component (call it `OnboardingForm`) and 21 | then make a new `App` which renders the `ErrorBoundary` around the `OnboardingForm` 22 | component along with our `FallbackComponent` called `ErrorFallback` (which you 23 | also have to create). 24 | 25 | Get to it! 26 | -------------------------------------------------------------------------------- /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/05.typescript/01.solution.props/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | const operations = { 4 | '+': (left: number, right: number): number => left + right, 5 | '-': (left: number, right: number): number => left - right, 6 | '*': (left: number, right: number): number => left * right, 7 | '/': (left: number, right: number): number => left / right, 8 | } 9 | 10 | type CalculatorProps = { 11 | left: number 12 | operator: string 13 | right: number 14 | } 15 | function Calculator({ left, operator, right }: CalculatorProps) { 16 | // @ts-expect-error we'll fix this one later 17 | const result = operations[operator](left, right) 18 | return ( 19 |
20 | 21 | {left} {operator} {right} = {result} 22 | 23 |
24 | ) 25 | } 26 | 27 | function App() { 28 | return ( 29 |
30 |

Calculator

31 | 32 | 33 | 34 | 35 |
36 | ) 37 | } 38 | 39 | const rootEl = document.createElement('div') 40 | document.body.append(rootEl) 41 | createRoot(rootEl).render() 42 | -------------------------------------------------------------------------------- /exercises/07.forms/03.solution.types/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | return ( 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 |
30 | 31 |
32 | ) 33 | } 34 | 35 | const rootEl = document.createElement('div') 36 | document.body.append(rootEl) 37 | createRoot(rootEl).render() 38 | -------------------------------------------------------------------------------- /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/08.inputs/03.problem.radio/README.mdx: -------------------------------------------------------------------------------- 1 | # Radios 2 | 3 | 4 | 5 | 👨‍💼 We need to allow the user to select the visibility of the account. It can be 6 | either Public or Private. We could use a ` 35 |
  • 36 | ))} 37 |
38 |
39 | ) 40 | } 41 | 42 | const rootEl = document.createElement('div') 43 | document.body.append(rootEl) 44 | createRoot(rootEl).render() 45 | -------------------------------------------------------------------------------- /exercises/07.forms/05.solution.action/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | function logFormData(formData: FormData) { 5 | console.log(Object.fromEntries(formData)) 6 | } 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 | ) 36 | } 37 | 38 | const rootEl = document.createElement('div') 39 | document.body.append(rootEl) 40 | createRoot(rootEl).render() 41 | -------------------------------------------------------------------------------- /exercises/10.arrays/01.solution.key-prop/README.mdx: -------------------------------------------------------------------------------- 1 | # Key prop 2 | 3 | 4 | 5 | 👨‍💼 Great work. That was super weird and confusing for our users. They're much 6 | happier now. 7 | 8 | 🦉 The key only needs to be unique within a given array. So this works 9 | fine: 10 | 11 | ```tsx 12 | const element = ( 13 |
    14 | {list.map((listItem) => ( 15 |
  • {listItem.value}
  • 16 | ))} 17 |
18 | ) 19 | ``` 20 | 21 | In our example, the `value` of the input is managed by the browser, but this 22 | has even bigger implications when we start working with our own state and 23 | side-effects. It's a little too early to demonstrate this for you, but you 24 | should know that when React removes a component from the DOM, it gets 25 | "unmounted" which will trigger side-effect cleanups, and if new elements are 26 | added then those will be "mounted" and will trigger your side-effects. This can 27 | cause some surprising and problematic issues for your users. 28 | 29 | So just remember the rule and always provide a `key` when rendering an array. 30 | Later when you have more React experience, you can come back to this exercise 31 | and expand it a bit with custom components that manage state and side-effects to 32 | observe the problems caused when you ignore the `key`. 33 | -------------------------------------------------------------------------------- /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/05.typescript/06.problem.satisfies/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | type OperationFn = (left: number, right: number) => number 4 | // 💣 delete the Operator type 5 | type Operator = '+' | '-' | '/' | '*' 6 | // 🦺 remove the type cast 7 | const operations: Record = { 8 | '+': (left, right) => left + right, 9 | '-': (left, right) => left - right, 10 | '*': (left, right) => left * right, 11 | '/': (left, right) => left / right, 12 | } 13 | // 🦺 add "satisfies" here to ensure operations satisfies a Record with string keys and OperationFn values 14 | 15 | type CalculatorProps = { 16 | left?: number 17 | operator?: keyof typeof operations 18 | right?: number 19 | } 20 | function Calculator({ left = 0, operator = '+', right = 0 }: CalculatorProps) { 21 | const result = operations[operator](left, right) 22 | return ( 23 |
    24 | 25 | {left} {operator} {right} = {result} 26 | 27 |
    28 | ) 29 | } 30 | 31 | function App() { 32 | return ( 33 |
    34 |

    Calculator

    35 | 36 | 37 | 38 | 39 |
    40 | ) 41 | } 42 | 43 | const rootEl = document.createElement('div') 44 | document.body.append(rootEl) 45 | createRoot(rootEl).render() 46 | -------------------------------------------------------------------------------- /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/09.errors/03.solution.reset/error-boundary.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' 2 | const { screen } = dtl 3 | import { userEvent } from '@testing-library/user-event' 4 | 5 | const user = userEvent.setup() 6 | 7 | import './index.tsx' 8 | 9 | await testStep('Error boundary is rendered after form submission', async () => { 10 | // submit the form 11 | await user.click(await screen.findByRole('button', { name: /submit/i })) 12 | 13 | // Check if the error message is displayed 14 | const errorMessage = await screen.findByRole('alert') 15 | expect(errorMessage).toBeDefined() 16 | expect(errorMessage.textContent).toContain('There was an error:') 17 | 18 | // Ensure the form is not rendered after error 19 | const form = screen.queryByRole('form') 20 | expect(form).toBeNull() 21 | }) 22 | 23 | await testStep('Clicking "Try again" resets the error boundary', async () => { 24 | // Click the "Try again" button 25 | const tryAgainButton = await screen.findByRole('button', { 26 | name: /try again/i, 27 | }) 28 | await user.click(tryAgainButton) 29 | 30 | // Verify that the submit button appears again 31 | const submitButton = await screen.findByRole('button', { name: /submit/i }) 32 | expect(submitButton).toBeDefined() 33 | 34 | // Ensure the error message is no longer present 35 | const errorMessage = screen.queryByRole('alert') 36 | expect(errorMessage).toBeNull() 37 | }) 38 | -------------------------------------------------------------------------------- /exercises/04.components/04.solution.props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
      4 | 5 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /exercises/08.inputs/01.problem.checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | function App() { 4 | function logFormData(formData: FormData) { 5 | console.log(Object.fromEntries(formData)) 6 | } 7 | return ( 8 |
      9 |
      10 | 11 | 12 |
      13 |
      14 | 15 | 16 |
      17 |
      18 | 19 | 20 |
      21 |
      22 | 23 | 24 |
      25 |
      26 | 27 | 28 |
      29 | {/* 🐨 add a checkbox named "waiver" with the label "Waiver Signed" */} 30 | {/* 💰 put the inside the