├── .github └── workflows │ └── next.yaml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── components ├── corner.js ├── footer.js ├── github.js ├── header.js ├── layout.js ├── linkedin.js └── twitter.js ├── context ├── courseInfoContext.js └── headerContext.js ├── course.json ├── csv └── index.js ├── data ├── course.js └── lesson.js ├── lessons ├── 01-welcome │ └── A-intro.md ├── 02-no-frills-react │ ├── A-pure-react.md │ ├── B-components.md │ └── meta.json ├── 03-js-tools │ ├── A-npm.md │ ├── B-prettier.md │ ├── C-eslint.md │ ├── D-git.md │ ├── E-vite.md │ └── meta.json ├── 04-core-react-concepts │ ├── A-jsx.md │ ├── B-hooks.md │ ├── C-effects.md │ ├── D-custom-hooks.md │ ├── E-handling-user-input.md │ ├── F-component-composition.md │ ├── G-react-dev-tools.md │ └── meta.json ├── 05-react-capabilities │ ├── A-react-router.md │ ├── B-react-query.md │ ├── C-uncontrolled-forms.md │ ├── D-class-components.md │ └── meta.json ├── 06-special-case-react-tools │ ├── A-error-boundaries.md │ ├── B-portals-and-refs.md │ ├── C-context.md │ └── meta.json ├── 07-end-of-intro │ ├── A-conclusion.md │ ├── B-ways-to-expand-your-app.md │ └── meta.json ├── 08-intermediate-react-v5 │ ├── A-welcome-to-intermediate-react-v5.md │ └── meta.json ├── 09-hooks-in-depth │ ├── A-useref.md │ ├── B-usereducer.md │ ├── C-usememo.md │ ├── D-usecallback.md │ ├── E-uselayouteffect.md │ ├── F-useid.md │ ├── G-others.md │ └── meta.json ├── 10-tailwindcss │ ├── A-css-and-react.md │ ├── B-tailwind-basics.md │ ├── C-tailwind-plugins.md │ ├── D-apply.md │ ├── E-grid-and-breakpoints.md │ ├── F-positioning.md │ └── meta.json ├── 11-advance-react-performance │ ├── A-code-splitting.md │ ├── B-server-side-rendering.md │ └── meta.json ├── 12-low-priority-rerendering │ ├── A-deferred-values.md │ ├── B-transitions.md │ └── meta.json ├── 13-typescript │ ├── A-refactor-modal.md │ ├── B-typescript-and-eslint.md │ ├── C-refactor-details.md │ ├── D-refactor-adopted-pet-context.md │ ├── E-refactor-error-boundary.md │ ├── F-refactor-carousel.md │ ├── G-refactor-pet.md │ ├── H-refactor-fetches.md │ ├── I-refactor-breed-list.md │ ├── J-refactor-search-params.md │ ├── K-refactor-results.md │ ├── M-refactor-app.md │ └── meta.json ├── 14-redux │ ├── A-redux-toolkit.md │ ├── B-more-app-state.md │ ├── C-rtk-query.md │ ├── D-redux-dev-tools.md │ └── meta.json ├── 15-testing │ ├── A-testing-react.md │ ├── B-basic-react-testing.md │ ├── C-testing-ui-interactions.md │ ├── D-testing-custom-hooks.md │ ├── E-mocks.md │ ├── F-snapshots.md │ ├── G-c8.md │ ├── H-visual-studio-code-extension.md │ └── meta.json └── 16-end-of-intermediate │ ├── A-end-of-intermediate.md │ └── meta.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── index.js └── lessons │ └── [section] │ └── [slug].js ├── public ├── .nojekyll └── images │ ├── apple-touch-icon.png │ ├── author.jpg │ ├── course-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── social-share-cover.jpg └── styles ├── courses.css ├── footer.css └── variables.css /.github/workflows/next.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy NextJS Course Site to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: npm install, export 14 | run: | 15 | npm install 16 | npm run export 17 | - name: Deploy site to gh-pages branch 18 | uses: crazy-max/ghaction-github-pages@v2 19 | with: 20 | target_branch: gh-pages 21 | build_dir: out 22 | fqdn: react-v8.holt.courses 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | *.csv 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

react logo

2 | 3 | [![Frontend Masters](https://static.frontendmasters.com/assets/brand/logos/full.png)][fem] 4 | 5 | [Please click here][course] to head to the course website. 6 | 7 | # Issues and Pull Requests 8 | 9 | Please file issues and open pull requests here! Thank you! For issues with project files, either file issues on _this_ repo _or_ open a pull request on the projects repos. This repo itself is the course website. 10 | 11 | # Project Files 12 | 13 | [Please go here][project] for the project files. 14 | 15 | # License 16 | 17 | The content of this workshop is licensed under CC-BY-NC-4.0. Feel free to share freely but do not resell my content. 18 | 19 | The code, including the code of the site itself and the code in the exercises, are licensed under Apache 2.0. 20 | 21 | [fem]: https://frontendmasters.com/workshops/complete-react-v8/ 22 | [course]: https://react-v8.holt.courses 23 | [project]: https://github.com/btholt/citr-v8-project/ 24 | 25 | [React icons created by Pixel perfect - Flaticon](https://www.flaticon.com/free-icons/react) 26 | -------------------------------------------------------------------------------- /components/corner.js: -------------------------------------------------------------------------------- 1 | export default function Corner() { 2 | return ( 3 |
4 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Gh from "./github"; 3 | import Tw from "./twitter"; 4 | import Li from "./linkedin"; 5 | 6 | export default function Footer({ twitter, linkedin, github }) { 7 | return ( 8 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/github.js: -------------------------------------------------------------------------------- 1 | export default function GitHub() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import Link from "next/link"; 3 | import { Context as HeaderContext } from "../context/headerContext"; 4 | import { Context as CourseContext } from "../context/courseInfoContext"; 5 | 6 | export default function Header(props) { 7 | const [{ section, title, icon }] = useContext(HeaderContext); 8 | const { frontendMastersLink } = useContext(CourseContext); 9 | return ( 10 |
11 |

12 | {props.title} 13 |

14 |
15 | {frontendMastersLink ? ( 16 | 17 | Watch on Frontend Masters 18 | 19 | ) : null} 20 | {section ? ( 21 |

22 | {section} {title} 23 |

24 | ) : null} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/layout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import Footer from "./footer"; 4 | import Header from "./header"; 5 | import getCourseConfig from "../data/course"; 6 | import { Provider as HeaderProvider } from "../context/headerContext"; 7 | import { Provider as CourseInfoProvider } from "../context/courseInfoContext"; 8 | 9 | function Layout({ children }) { 10 | const courseInfo = getCourseConfig(); 11 | const headerHook = useState({}); 12 | return ( 13 | 14 | 15 |
16 |
17 |
18 |
{children}
19 |
20 |
25 |
26 | 27 | 34 |
35 |
36 | ); 37 | } 38 | 39 | export default function App({ children }) { 40 | return {children}; 41 | } 42 | -------------------------------------------------------------------------------- /components/linkedin.js: -------------------------------------------------------------------------------- 1 | export default function LinkedIn() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/twitter.js: -------------------------------------------------------------------------------- 1 | export default function Twitter() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /context/courseInfoContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const courseInfoContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = courseInfoContext.Provider; 6 | export const Consumer = courseInfoContext.Consumer; 7 | export const Context = courseInfoContext; 8 | -------------------------------------------------------------------------------- /context/headerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const headerContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = headerContext.Provider; 6 | export const Consumer = headerContext.Consumer; 7 | export const Context = headerContext; 8 | -------------------------------------------------------------------------------- /course.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Brian Holt", 4 | "company": "Stripe" 5 | }, 6 | "title": "Complete Intro to React v8", 7 | "subtitle": "and Intermediate React v5", 8 | "frontendMastersLink": "https://frontendmasters.com/courses/complete-react-v8/", 9 | "social": { 10 | "linkedin": "btholt", 11 | "github": "btholt", 12 | "twitter": "holtbt" 13 | }, 14 | "description": "Come learn React from Brian Holt, a veteran React.js developer on Frontend Masters", 15 | "keywords": ["react", "reactjs", "brian holt", "javascript", "node", "nodejs", "js", "redux", "tailwindcss", "testing", "typescript", "hooks", "vite"] 16 | } 17 | -------------------------------------------------------------------------------- /csv/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { convertArrayToCSV } from "convert-array-to-csv"; 4 | import { getLessons } from "../data/lesson.js"; 5 | 6 | async function start() { 7 | const configBuffer = await fs.readFile( 8 | path.join(process.cwd(), "course.json") 9 | ); 10 | const config = JSON.parse(configBuffer); 11 | 12 | if (!config.csvPath) { 13 | console.log("no csvPath in course.json, skipping CSV generation"); 14 | return; 15 | } 16 | 17 | process.env.BASE_URL = config?.productionBaseUrl || ""; 18 | const sections = await getLessons(); 19 | 20 | const lessons = []; 21 | 22 | for (let i = 0; i < sections.length; i++) { 23 | const section = sections[i]; 24 | 25 | for (let j = 0; j < section.lessons.length; j++) { 26 | const lesson = section.lessons[j]; 27 | 28 | lessons.push({ 29 | order: lesson.order, 30 | sectionTitle: section.title, 31 | lessonTitle: lesson.title, 32 | slug: section.slug + "/" + lesson.slug, 33 | sectionIcon: section.icon, 34 | filePath: lesson.fullSlug, 35 | description: lesson.description, 36 | }); 37 | } 38 | } 39 | 40 | const csv = convertArrayToCSV(lessons); 41 | 42 | await fs.writeFile(config.csvPath, csv); 43 | console.log(`wrote ${lessons.length} rows to ${config.csvPath}`); 44 | } 45 | 46 | start(); 47 | -------------------------------------------------------------------------------- /data/course.js: -------------------------------------------------------------------------------- 1 | import config from "../course.json"; 2 | 3 | const DEFAULT_CONFIG = { 4 | author: { 5 | name: "An Author", 6 | company: "An Author's Company", 7 | }, 8 | title: "A Superb Course", 9 | subtitle: "That Teaches Nice Things", 10 | frontendMastersLink: "", 11 | description: "A nice course for nice people.", 12 | keywords: ["a nice course", "for people", "to learn", "nice things"], 13 | social: { 14 | linkedin: "btholt", 15 | github: "btholt", 16 | twitter: "holtbt", 17 | }, 18 | productionBaseUrl: "/", 19 | }; 20 | 21 | export default function getCourseConfig() { 22 | return Object.assign({}, DEFAULT_CONFIG, config); 23 | } 24 | -------------------------------------------------------------------------------- /data/lesson.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import matter from "gray-matter"; 4 | import { titleCase } from "title-case"; 5 | import { marked } from "marked"; 6 | import hljs from "highlight.js"; 7 | 8 | const DEFAULT_ICON = "info-circle"; 9 | const lessonsPath = path.join(process.cwd(), "lessons"); 10 | 11 | function getTitle(slug, override) { 12 | let title = override; 13 | if (!title) { 14 | title = titleCase(slug.split("-").join(" ")); 15 | } 16 | 17 | return title; 18 | } 19 | 20 | async function getMeta(section) { 21 | let meta = {}; 22 | try { 23 | const file = await fs.readFile( 24 | path.join(lessonsPath, section, "meta.json") 25 | ); 26 | meta = JSON.parse(file.toString()); 27 | } catch (e) { 28 | // no meta.json, nothing to do 29 | } 30 | 31 | return meta; 32 | } 33 | 34 | function slugify(inputPath) { 35 | const pathParts = inputPath.split("-"); 36 | const pathOrder = pathParts.shift(); 37 | const pathSlug = pathParts.join("-"); 38 | return { 39 | slug: pathSlug, 40 | order: pathOrder, 41 | title: titleCase(pathParts.join(" ")), 42 | }; 43 | } 44 | 45 | export async function getLessons() { 46 | marked.setOptions({ 47 | baseUrl: process.env.BASE_URL ? process.env.BASE_URL + "/" : "/", 48 | highlight: function (code, lang) { 49 | const language = hljs.getLanguage(lang) ? lang : "plaintext"; 50 | return hljs.highlight(code, { language }).value; 51 | }, 52 | langPrefix: "hljs language-", 53 | }); 54 | 55 | const dir = await fs.readdir(lessonsPath); 56 | const sections = []; 57 | 58 | for (let dirFilename of dir) { 59 | const dirStats = await fs.lstat(path.join(lessonsPath, dirFilename)); 60 | 61 | if (dirStats.isFile()) { 62 | continue; 63 | } 64 | 65 | const lessonsDir = await fs.readdir(path.join(lessonsPath, dirFilename)); 66 | 67 | let { 68 | title: sectionTitle, 69 | order: sectionOrder, 70 | slug: sectionSlug, 71 | } = slugify(dirFilename); 72 | 73 | let icon = DEFAULT_ICON; 74 | 75 | const meta = await getMeta(dirFilename); 76 | if (meta.title) { 77 | sectionTitle = meta.title; 78 | } 79 | if (meta.icon) { 80 | icon = meta.icon; 81 | } 82 | 83 | const lessons = []; 84 | for (let lessonFilename of lessonsDir) { 85 | if (lessonFilename.slice(-3) !== ".md") { 86 | continue; 87 | } 88 | 89 | const filePath = path.join(lessonsPath, dirFilename, lessonFilename); 90 | 91 | const file = await fs.readFile(filePath); 92 | const { data } = matter(file.toString()); 93 | let slug = lessonFilename.replace(/\.md$/, ""); 94 | 95 | const slugParts = slug.split("-"); 96 | const lessonOrder = slugParts.shift(); 97 | 98 | slug = slugParts.join("-"); 99 | 100 | const title = getTitle(slug, data.title); 101 | 102 | lessons.push({ 103 | slug, 104 | fullSlug: `/lessons/${sectionSlug}/${slug}`, 105 | title, 106 | order: `${sectionOrder}${lessonOrder.toUpperCase()}`, 107 | path: filePath, 108 | description: data.description ? data.description : "", 109 | }); 110 | } 111 | 112 | sections.push({ 113 | icon, 114 | title: sectionTitle, 115 | slug: sectionSlug, 116 | lessons, 117 | order: sectionOrder, 118 | }); 119 | } 120 | 121 | return sections; 122 | } 123 | 124 | export async function getLesson(targetDir, targetFile) { 125 | const dir = await fs.readdir(lessonsPath); 126 | 127 | for (let i = 0; i < dir.length; i++) { 128 | const dirPath = dir[i]; 129 | if (dirPath.endsWith(targetDir)) { 130 | const lessonDir = ( 131 | await fs.readdir(path.join(lessonsPath, dirPath)) 132 | ).filter((str) => str.endsWith(".md")); 133 | 134 | for (let j = 0; j < lessonDir.length; j++) { 135 | const slugPath = lessonDir[j]; 136 | if (slugPath.endsWith(targetFile + ".md")) { 137 | const filePath = path.join(lessonsPath, dirPath, slugPath); 138 | const file = await fs.readFile(filePath); 139 | const { data, content } = matter(file.toString()); 140 | const html = marked(content); 141 | const title = getTitle(targetFile, data.title); 142 | const meta = await getMeta(dirPath); 143 | 144 | const section = getTitle(targetDir, meta.title); 145 | const icon = meta.icon ? meta.icon : DEFAULT_ICON; 146 | 147 | let nextSlug; 148 | let prevSlug; 149 | 150 | // get next 151 | if (lessonDir[j + 1]) { 152 | // has next in section 153 | const { slug: next } = slugify(lessonDir[j + 1]); 154 | nextSlug = `${targetDir}/${next.replace(/\.md$/, "")}`; 155 | } else if (dir[i + 1]) { 156 | // has next in next section 157 | const nextDir = ( 158 | await fs.readdir(path.join(lessonsPath, dir[i + 1])) 159 | ).filter((str) => str.endsWith(".md")); 160 | const nextDirSlug = slugify(dir[i + 1]).slug; 161 | const nextLessonSlug = slugify(nextDir[0]).slug.replace( 162 | /\.md$/, 163 | "" 164 | ); 165 | nextSlug = `${nextDirSlug}/${nextLessonSlug}`; 166 | } else { 167 | // last section 168 | nextSlug = null; 169 | } 170 | 171 | // get prev 172 | if (lessonDir[j - 1]) { 173 | // has prev in section 174 | const { slug: prev } = slugify(lessonDir[j - 1]); 175 | prevSlug = `${targetDir}/${prev.replace(/\.md$/, "")}`; 176 | } else if (dir[i - 1]) { 177 | // has prev in prev section 178 | const prevDir = ( 179 | await fs.readdir(path.join(lessonsPath, dir[i - 1])) 180 | ).filter((str) => str.endsWith(".md")); 181 | const prevDirSlug = slugify(dir[i - 1]).slug; 182 | const prevLessonSlug = slugify( 183 | prevDir[prevDir.length - 1] 184 | ).slug.replace(/\.md$/, ""); 185 | prevSlug = `${prevDirSlug}/${prevLessonSlug}`; 186 | } else { 187 | // first section 188 | prevSlug = null; 189 | } 190 | 191 | const base = process.env.BASE_URL ? process.env.BASE_URL : "/"; 192 | 193 | return { 194 | attributes: data, 195 | html, 196 | slug: targetFile, 197 | title, 198 | section, 199 | icon, 200 | filePath, 201 | nextSlug: nextSlug ? path.join(base, "lessons", nextSlug) : null, 202 | prevSlug: prevSlug ? path.join(base, "lessons", prevSlug) : null, 203 | }; 204 | } 205 | } 206 | } 207 | } 208 | 209 | return false; 210 | } 211 | -------------------------------------------------------------------------------- /lessons/02-no-frills-react/A-pure-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian teaches React without any frills: just you, some JavaScript, and the browser. No build step." 3 | --- 4 | 5 | Let's start by writing pure React. No compile step. No JSX. No Babel. No Webpack or Parcel. Just some JavaScript on a page. 6 | 7 | Let's start your project. Create your project directory. I'm going to call mine `adopt-me` since we're going to be building a pet adoption app throughout this course. Create an index.html and put it into a `src/` directory inside of your project folder. In index.html put: 8 | 9 | ```javascript 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Adopt Me 19 | 20 | 21 | 22 |
not rendered
23 | 24 | 25 | 28 | 29 | 30 | 31 | ``` 32 | 33 | > What's new between React 17 and React 18? A few things, here and there, but almost entirely additive and few things to change. We'll cover them over the arc of this course. 34 | 35 | Now open this file in your browser. On Mac, hit ⌘ (command) + O in your favorite browser, and on Windows and Linux hit CTRL + O to open the Open prompt. Navigate to wherever you saved the file and open it. You should see a line of text saying "not rendered". 36 | 37 | - Pretty standard HTML5 document. If this is confusing, I teach another course called [Intro to Web Dev][webdev] that can help you out. 38 | - We're adding a root div. We'll render our React app here in a sec. It doesn't _have_ to be called root, just a common practice. 39 | - We have two script tags. 40 | - The first is the React library. This library is the interface of how to interact with React; all the methods (except one) will be via this library. It contains no way of rendering itself though; it's just the API. 41 | - The second library is the rendering layer. Since we're rendering to the browser, we're using React DOM. There are other React libraries like React Native, React 360 (formerly React VR), A-Frame React, React Blessed, and others. You need both script tags. The order is not important. 42 | - The last script tag is where we're going to put our code. You don't typically do this but I wanted to start as simple as possible. This script tag must come _after_ the other two. 43 | 44 | > Let's add some style! [Click here][style] to get the stylesheet for this course. Make a file called style.css in src/ and paste the previous file there. If you follow along with the course and use the same class names, the styles will be applied for you automatically. This isn't a course on CSS so I make no assertion it's any good! 45 | 46 | In the last script tag, put the following. 47 | 48 | ```javascript 49 | const App = () => { 50 | return React.createElement( 51 | "div", 52 | {}, 53 | React.createElement("h1", {}, "Adopt Me!") 54 | ); 55 | }; 56 | 57 | const container = document.getElementById("root"); 58 | const root = ReactDOM.createRoot(container); 59 | root.render(React.createElement(App)); 60 | ``` 61 | 62 | This is about the simplest React app you can build. 63 | 64 | - The first thing we do is make our own component, App. React is all about making components. And then taking those components and making more components out of those. 65 | - There are two types of components, function components and class components. This is a function component. We'll see class components shortly. 66 | - A function component _must_ return markup (which is what `React.createElement` generates.) 67 | - These component render functions _have_ to be fast. This function is going to be called a lot. It's a hot code path. 68 | - Inside of the render function, you cannot modify any sort of state. Put in functional terms, this function must be pure. You don't know how or when the function will be called so it can't modify any ambient state. 69 | - `React.createElement` creates one _instance_ of some component. If you pass it a _string_, it will create a DOM tag with that as the string. We used `h1` and `div`, those tags are output to the DOM. If we put `x-custom-date-picker`, it'll output that (so web components are possible too.) 70 | - The second empty object (you can put `null` too) is attributes we're passing to the tag or component. Whatever we put in this will be output to the element (like id or style.) 71 | - First we're using `document.getElementById` to grab an existing div out of the HTML document. Then we take that element (which we called `container`) and pass that into `ReactDOM.createRoot`. This is how we signal to React where we want it to render our app. Note later we can `root.render` again to change what the root of our React app looks like (I rarely need to do that.) 72 | - Notice we're using `React.createElement` with `App` as a parameter to `root.render`. We need an _instance_ of `App` to render out. `App` is a class of components and we need to render one instance of a class. That's what `React.createElement` does: it makes an instance of a class. An analogy is that `App` as a _class_ of components is like Honda has a line of cars called Civics. It's a whole line of cars with various different options and parameters. An _instance_ of a Civic would be one individual car. It's a concrete instance of the Civic car line. 73 | 74 | > ReactDOM.createRoot is a new API as of React v18. The old `ReactDOM.render` is still available (and deprecated) but it'll render your app in "legacy" mode which won't use all the fun new features packed into React v18 75 | 76 | [webdev]: https://frontendmasters.com/courses/web-development-v3/ 77 | [style]: https://raw.githubusercontent.com/btholt/citr-v8-project/master/01-no-frills-react/src/style.css 78 | -------------------------------------------------------------------------------- /lessons/02-no-frills-react/B-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian teaches React without any frills: just you, some JavaScript, and the browser. No build step." 3 | --- 4 | 5 | Now that we've done that, let's separate this out from a script tag on the DOM to its own script file (best practice.) Make a new file in your `src` directory called `App.js` and cut and paste your code into it. 6 | 7 | Modify your code so it looks like: 8 | 9 | ```javascript 10 | const Pet = () => { 11 | return React.createElement("div", {}, [ 12 | React.createElement("h1", {}, "Luna"), 13 | React.createElement("h2", {}, "Dog"), 14 | React.createElement("h2", {}, "Havanese"), 15 | ]); 16 | }; 17 | 18 | const App = () => { 19 | return React.createElement("div", {}, [ 20 | React.createElement("h1", {}, "Adopt Me!"), 21 | React.createElement(Pet), 22 | React.createElement(Pet), 23 | React.createElement(Pet), 24 | ]); 25 | }; 26 | 27 | const container = document.getElementById("root"); 28 | const root = ReactDOM.createRoot(container); 29 | root.render(React.createElement(App)); 30 | ``` 31 | 32 | > 🚨 You will be seeing a console warning `Warning: Each child in a list should have a unique "key" prop.` in your browser console. React's dev warnings are trying to help your code run faster. Basically, React tries to keep track of components that are swapped in order. In a list, it does that by you giving it a unique key it can track. If it sees two things have swapped, it'll just move the components instead of re-rendering. 33 | 34 | Replace your `script` tag in your index.html that has all your code in it with ``. Leave the two React scripts. 35 | 36 | - To make an element have multiple children, just pass it an array of elements. 37 | - We created a second new component, the `Pet` component. This component represents one pet. When you have distinct ideas represented as markup, that's a good idea to separate that it into a component like we did here. 38 | - Since we have a new `Pet` component, we can use it multiple times! We just use multiple calls to `React.createElement`. 39 | - In `createElement`, the last two parameters are optional. Since Pet has no props or children (it could, we just didn't make it use them yet) we can just leave them off. 40 | 41 | Okay so we can have multiple pets but it's not a useful component yet since not all pets will be Havanese dogs named Luna (even though _I_ have a Havanese dog named Luna.) Let's make it a bit more complicated. 42 | 43 | ```javascript 44 | const Pet = (props) => { 45 | return React.createElement("div", {}, [ 46 | React.createElement("h1", {}, props.name), 47 | React.createElement("h2", {}, props.animal), 48 | React.createElement("h2", {}, props.breed), 49 | ]); 50 | }; 51 | 52 | const App = () => { 53 | return React.createElement("div", {}, [ 54 | React.createElement("h1", {}, "Adopt Me!"), 55 | React.createElement(Pet, { 56 | name: "Luna", 57 | animal: "Dog", 58 | breed: "Havanese", 59 | }), 60 | React.createElement(Pet, { 61 | name: "Pepper", 62 | animal: "Bird", 63 | breed: "Cockatiel", 64 | }), 65 | React.createElement(Pet, { name: "Doink", animal: "Cat", breed: "Mix" }), 66 | ]); 67 | }; 68 | 69 | const container = document.getElementById("root"); 70 | const root = ReactDOM.createRoot(container); 71 | root.render(React.createElement(App)); 72 | ``` 73 | 74 | Now we have a more flexible component that accepts props from its parent. Props are variables that a parent (App) passes to its children (the instances of Pet.) Now each one can be different! Now that is far more useful than it was since this Pet component can represent not just Luna, but any Pet. This is the power of React! We can make multiple, re-usable components. We can then use these components to build larger components, which in turn make up yet-larger components. This is how React apps are made! 75 | 76 | > 🏁 [Click here to see the state of the project up until now: 01-no-frills-react][step] 77 | 78 | [step]: https://github.com/btholt/citr-v8-project/tree/master/01-no-frills-react 79 | -------------------------------------------------------------------------------- /lessons/02-no-frills-react/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "eye" 3 | } -------------------------------------------------------------------------------- /lessons/03-js-tools/A-npm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "npm" 3 | description: "When putting any sort of JavaScript project together, npm is an essential tool. Brian talks about how to get started with npm." 4 | --- 5 | 6 | ## npm 7 | 8 | npm does not stand for Node.js Package Manager. It is, however, the package manager for Node.js. (They don't say what it stands for.) It also has all the packages in the front end scene. npm makes a command line tool, called `npm` as well. `npm` allows you to bring in code from the npm registry which is a bunch of open source modules that people have written so you can use them in your project. Whenever you run `npm install react` (don't do this yet), it will install the latest version of React from the registry. 9 | 10 | In order to start an npm project, run `npm init -y` at the root of your project. If you don't have Node.js installed, please go install that too. When you run `npm init` it'll ask you a bunch of questions. If you don't know the answer or don't care, just hit enter. You can always modify package.json later. This will allow us to get started installing and saving packages. 11 | -------------------------------------------------------------------------------- /lessons/03-js-tools/B-prettier.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian talks about his favorite JS tool, Prettier, a tool that that helps you maintain consistent code style with no work on the dev's part." 3 | --- 4 | 5 | ## Code Quality 6 | 7 | It's important to keep quality high when writing code. Or at least that's how I sell ESLint and Prettier to my co-workers. In reality I'm super lazy and want the machine to do as much work as possible so I can focus more on architecture and problem-solving and less on syntax and style. While there are many tools that can help you keep code quality high, these two I consider core to my workflow. 8 | 9 | [Prettier][prettier] is an amazing tool from the brain of [James Long][jlongster]. James, like many of us, was sick of having to constantly worry about the style of his code: where to stick indents, how many, when to break lines, etc etc. Coming from languages like Go, Reason, or Elm where all that is just taken care of by the tooling for the language, this quickly wears. James did something about it and made a tool to take care of it: Prettier. 10 | 11 | Prettier is a really fancy pretty printer. It takes the code you write, breaks it down in to an abstract syntax tree (AST) which is just a representation of your code. It then takes that AST, throws away all of your code style you made and prints it back out using a predefined style. While this sounds a little scary, it's actually really cool. Since you no longer have control of the style of your code, you no longer have to think about it at all. Your code is always consistent, as is the code from the rest of your team. No more bikeshedding!! As I like to put it: if your brain is a processor, you get to free up the thread of your brain that worries about code styles and readability: it just happens for you. Don't like semicolons? Don't write them! It puts them in for you. I _love_ Prettier. 12 | 13 | Need to tool around a bit with it before you trust it? [Go here][prettier-playground]. You can see how it works. 14 | 15 | Let's go integrate this into our project. It's _pretty_ easy (since I'm a dad now, I'm legally obligated to make this joke.) 16 | 17 | Either install Prettier globally `npm install --global prettier` or replace when I run `prettier` with (from the root of your project) `npx prettier`. From there, run `prettier src/App.js`. This will output the formatted version of your file. If you want to actually write the file, run `prettier --write src/App.js`. Go check src/App.js and see it has been reformatted a bit. I will say for non-JSX React, prettier makes your code less readable. Luckily Prettier supports JSX! We'll get to that shortly. 18 | 19 | Prettier has a few configurations but it's mostly meant to be a tool everyone uses and doesn't argue/bikeshed about the various code style rules. [Here they are][prettier-options]. I just use it as is since I'm lazy. Prettier can also understand [flow][flow] and [TypeScript][ts]. 20 | 21 | Prettier is great to use with [Visual Studio Code][vscode]. Just download [this extension][vscode-prettier]. Pro tip: set it to only run Prettier when it detects a Prettier config file. Makes it so you never have to turn it off. In order to do that, set `prettier.requireConfig` to `true` and `editor.formatOnSave` to true. 22 | 23 | So that our tool can know this is a Prettier project, we're going to create a file called `.prettierrc` and put `{}` in it. This lets everyone know this is a Prettier project that uses the default configuration. You can put other configs here if you hold strong formatting opinions. 24 | 25 | ## npm/Yarn scripts 26 | 27 | So it can be painful to try to remember the various CLI commands to run on your project. You can put CLI commands into it and then run the name of the tag and it'll run that script. Let's go see how that works. Put the following into your package.json. 28 | 29 | First run `npm install -D prettier@2.7.1` `-D` means it's for development only. 30 | 31 | ```json 32 | "scripts": { 33 | "format": "prettier --write \"src/**/*.{js,jsx}\"" 34 | }, 35 | ``` 36 | 37 | Now you can run `yarn format` or `npm run format` and it will run that command. This means we don't have to remember that mess of a command and just have to remember format. Nice, right? We'll be leaning on this a lot during this course. 38 | 39 | > Note the `@2.7.1` portion. For the purposes of making this course not break in the future, I have you install the _exact_ version of packages I used when I made this course. As is natural, packages change and progress over time and I can't anticipate how that will happen. So I'd suggest you use the same packages I do as you do this course (even if npm yells at you for security vulnerabilites). As soon as you're done with the course, feel free to go update the versions to the latest and see if anything breaks. 40 | 41 | [jlongster]: https://twitter.com/jlongster 42 | [prettier]: https://github.com/prettier/prettier 43 | [prettier-playground]: https://prettier.io/playground/ 44 | [prettier-options]: https://prettier.io/docs/en/options.html 45 | [flow]: https://flow.org/ 46 | [prettier-ide]: https://github.com/prettier/prettier#editor-integration 47 | [ts]: https://www.typescriptlang.org/ 48 | [vscode]: https://code.visualstudio.com/?WT.mc_id=reactintro-github-brholt 49 | [vscode-prettier]: https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode&WT.mc_id=reactintro-github-brholt 50 | -------------------------------------------------------------------------------- /lessons/03-js-tools/C-eslint.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ESLint" 3 | description: "An essential part of maintaining a project a long time is discipline in coding standards and avoiding antipatterns. ESLint is a great tool that helps you do just that." 4 | --- 5 | 6 | On top of Prettier which takes of all the formatting, you may want to enforce some code styles which pertain more to usage: for example you may want to force people to never use `with` which is valid JS but ill advised to use. [ESLint][eslint] comes into play here. It will lint for these problems. 7 | 8 | First of all, run `npm install -D eslint@8.24.0 eslint-config-prettier@8.5.0` to install eslint in your project development dependencies. Then you may configure its functionalities. 9 | 10 | There are dozens of preset configs for ESLint and you're welcome to use any one of them. The [Airbnb config][airbnb] is very popular, as is the standard config (both of which I taught in previous versions of this class). I'm going to use a looser one for this class: `eslint:recommended`. Let's create an `.eslintrc.json` file to start linting our project. 11 | 12 | Create this file called `.eslintrc.json`. 13 | 14 | ```json 15 | { 16 | "extends": ["eslint:recommended", "prettier"], 17 | "plugins": [], 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | } 30 | } 31 | ``` 32 | 33 | This is a combination of the recommended configs of ESLint and Prettier. This will lint for both normal JS stuff as well as JSX stuff. Run `npx eslint src/App.js` now and you should see we have a few errors. Run it again with the `--fix` flag and see it will fix some of it for us! Go fix the rest of your errors and come back. Let's go add this to our npm scripts. 34 | 35 | ```json 36 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 37 | ``` 38 | 39 | > 🚨 ESLint will have a bunch of errors right now. Ignore them; we'll fix them in a sec. 40 | 41 | Worth adding three things here: 42 | 43 | - With npm scripts, you can pass additional parameters to the command if you want. Just add a `--` and then put whatever else you want to tack on after that. For example, if I wanted to get the debug output from ESLint, I could run `npm run lint -- --debug` which would translate to `eslint **/*.js --debug`. 44 | - We can use our fix trick this way: `npm run lint -- --fix`. 45 | - We're going to both JS and JSX. 46 | 47 | ESLint is a cinch to get working with [Visual Studio Code][vscode]. Just download [the extension][vscode-eslint]. 48 | 49 | ## Alternatives 50 | 51 | - [jshint][jshint] 52 | 53 | [eslint]: https://eslint.org 54 | [vscode-eslint]: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint 55 | [airbnb]: https://github.com/airbnb/javascript 56 | [jshint]: http://jshint.com/ 57 | [vscode]: https://code.visualstudio.com/ 58 | -------------------------------------------------------------------------------- /lessons/03-js-tools/D-git.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Git is a critical part of any JS project and Brian makes sure you have it set up." 3 | --- 4 | 5 | Git is a critical part of any project and probably something many of you are already familiar with. If you haven't, be sure to initialize your project as a git repo with `git init` in the root of your project (VSCode and any other number of tools can do this as well.) 6 | 7 | If you haven't already, create a .gitignore at the root of your project to ignore the stuff we don't want to commit. Go ahead and put this in there: 8 | 9 | ``` 10 | node_modules 11 | dist/ 12 | .env 13 | .DS_Store 14 | coverage/ 15 | .vscode/ 16 | ``` 17 | 18 | This will make it so these things won't get added to our repo. If you want more Git instruction, please check out [Nina Zakharenko's course on Frontend Masters][nina] 19 | 20 | [nina]: https://frontendmasters.com/courses/git-in-depth/ 21 | -------------------------------------------------------------------------------- /lessons/03-js-tools/E-vite.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Setting up a build process has historically been a big barrier to entry for most developers. Brian shows you how to set up Vite which makes the whole process a breeze." 3 | --- 4 | 5 | The build tool we are going to be using today is called [Vite][vite]. Vite (pronounced "veet", meaning quick in French) is a tool put out by the Vue team that ultimately ends up wrapping [Rollup][rollup] which does the actual bundling. The end result is a tool that is both easy to use and produces a great end result. 6 | 7 | Our end result that we want from a build tool is that 8 | 9 | - We can separate files out for code organization and have a tool stitch them together for us 10 | - We can include external, third-party libraries from npm (like React!) 11 | - The tool will optimize the code for us by minifying and other optimizing techniques 12 | 13 | Previous versions of this course used [Parcel][parcel], another tool near-and-dear to my heart. It is still an amazing tool and one I recommend you check out. We ended up moving to Vite because the React community has selected it as the tool-of-choice for the moment and this course aims to give you the community norms of React. Even older versions of this course previously taught [Webpack][webpack]. 14 | 15 | First, let's install the things we need for Vite. 16 | 17 | ```bash 18 | npm install -D vite@3.1.4 @vitejs/plugin-react@2.1.0 19 | ``` 20 | 21 | The former is the tool itself and the latter is all the React specific features we will need. Now that we have those installed, we need to modify our index.html just a little bit. 22 | 23 | ```html 24 | 25 | 26 | ``` 27 | 28 | We need to add module to the script tag so that the browser knows it's working with modern browser technology that allows you in development mode to use modules directly. Instead of having to reload the whole bundle every time, your browser can just reload the JS that has changed. It allows the browser to crawl the dependency graph itself which means Vite can run lightning fast in dev mode. It will still package it up for production so we can support a range of browsers. 29 | 30 | Next, let's make our config file. Make a file in the root of your project called `vite.config.js` and stick this in there: 31 | 32 | ```javascript 33 | import { defineConfig } from "vite"; 34 | import react from "@vitejs/plugin-react"; 35 | 36 | export default defineConfig({ 37 | plugins: [react()], 38 | root: "src", 39 | }); 40 | ``` 41 | 42 | We add the `react` plugin to Vite and we set our `root` directory to be our `src` directory. Generally your root is going to be where-ever you keep your index.html. Many projects will just keep the index.html file in the root of the project for this reason. I consider it a source file, so I keep it in src. 43 | 44 | By default, Vite will find the index.html file in where-ever the root is and treat it as the head of a source graph. It'll crawl all your HTML, CSS, and JavaScript you link to from there and create your project for you. We don't have to do any more configuration than that. Vite will take care of the rest. 45 | 46 | Okay, let's _actually_ install React to our project 47 | 48 | ```bash 49 | npm install react@18.2.0 react-dom@18.2.0 50 | ``` 51 | 52 | - We did not include the `-D` because React is not a development tool, it's a production dependency 53 | - React and ReactDOM are versioned together so you can assume those versions will always be the same 54 | 55 | Finally, head to App.js and modify the following 56 | 57 | ```javascript 58 | // add to the top 59 | import React from "react"; 60 | import { createRoot } from "react-dom/client"; 61 | 62 | // modify the createRoot call, delete "ReactDOM" 63 | const root = createRoot(container); 64 | ``` 65 | 66 | Now let's set up our scripts to start Vite. In package.json, put: 67 | 68 | ```json 69 | // inside scripts 70 | "dev": "vite", 71 | "build": "vite build", 72 | "preview": "vite preview" 73 | ``` 74 | 75 | `dev` will start the development server, typically on [http://localhost:5173/](). `build` will prepare static files to be deployed (to somewhere like GitHub Pages, Vercel, Netlify, AWS S3, etc.) `preview` lets you preview your production build locally. 76 | 77 | > Please close the `file:///` browser tabs you have open and only use the `localhost:1234` ones. Now that we're using Vite the former won't work anymore! If you see something about `CORS` errors in your console it's because you're probably still looking at the file:/// version and not the local dev server 78 | 79 | ## Alternatives 80 | 81 | There are a myriad of fantastic developer tools out there available. We chose Vite because the industry has been using it for a while but I have zero problem with you selecting other tools. Just trying to expose everyone to great tools. 82 | 83 | > 🏁 [Click here to see the state of the project up until now: 02-js-tools][step]. 84 | 85 | [step]: https://github.com/btholt/citr-v8-project/tree/master/02-js-tools 86 | [webpack]: https://webpack.js.org/ 87 | [parcel]: https://parceljs.org/ 88 | [rollup]: https://www.rollupjs.org/ 89 | [vite]: https://vitejs.dev/ 90 | -------------------------------------------------------------------------------- /lessons/03-js-tools/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "JS Tools", 3 | "icon": "hammer" 4 | } -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/A-jsx.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "JSX" 3 | description: "JSX is an essential part of writing React. Brian teaches you to leverage your newfound React knowledge and make it much easier to read with JSX" 4 | --- 5 | 6 | So far we've been writing React without JSX, something that I don't know anyone that actually does with their apps. _Everyone_ uses JSX. I show you this way so what JSX is actually doing is demystified to you. It doesn't do hardly anything. It just makes your code a bit more readable. 7 | 8 | If I write `React.createElement("h1", { id: "main-title" }, "My Website");`, what am I actually trying to have rendered out? `

My Website

`, right? What JSX tries to do is to shortcut this translation layer in your brain so you can just write what you mean. 9 | 10 | Make a new file called Pet.jsx. 11 | 12 | > Make sure you call it `.jsx` and not `.js`. Vite won't do JSX transpilation if it's not named with a JSX file extension. 13 | 14 | ```javascript 15 | const Pet = (props) => { 16 | return ( 17 |
18 |

{props.name}

19 |

{props.animal}

20 |

{props.breed}

21 |
22 | ); 23 | }; 24 | 25 | export default Pet; 26 | ``` 27 | 28 | > 🚨 ESLint may be currently failing. We'll fix it at the end. 29 | 30 | I don't know about you, but I find this far more readable. And if it feels uncomfortable to you to introduce HTML into your JavaScript, I invite you to give it a shot until the end of the workshop. By then it should feel a bit more comfortable. And you can always go back to the old way. 31 | 32 | However, now you know _what_ JSX is doing for you. It's just translating those HTML tags into `React.createElement` calls. _That's it._ Really. No more magic here. JSX does nothing else. Many people who learn React don't learn this. 33 | 34 | Notice the strange `{props.name}` syntax: this is how you output JavaScript expressions in JSX. An expression is anything that can be the right side of an assignment operator in JavaScript, e.g. `const x = `. If you take away the `{}` it will literally output `props.name` to the DOM. 35 | 36 | > Notice we don't have to do `import React from 'react'` here like we used to. The latest version of JSX handles that for you so you only need to explicitly import the React package when you need to use something from it; otherwise feel free to do JSX without having to import React! 37 | 38 | So now JSX is demystified a bit, let's go convert App.js. 39 | 40 | ```javascript 41 | // rename the file App.jsx 42 | // delete the React import 43 | import { createRoot } from "react-dom/client"; 44 | import Pet from "./Pet"; 45 | 46 | // delete the Pet component 47 | 48 | const App = () => { 49 | return ( 50 |
51 |

Adopt Me!

52 | 53 | 54 | 55 |
56 | ); 57 | }; 58 | 59 | const container = document.getElementById("root"); 60 | const root = createRoot(container); 61 | root.render(); 62 | ``` 63 | 64 | > 🚨 ESLint is currently failing. We'll fix it at the end. 65 | 66 | Also head over to index.html and change the script tag 67 | 68 | ```html 69 | 70 | ``` 71 | 72 | Notice we have Pet as a component. Notice that the `P` in `Pet` is capitalized. It _must_ be. If you make it lowercase, it will try to have `pet` as a web component and not a React component. 73 | 74 | We now pass props down as we add attributes to an HTML tag. Pretty cool. 75 | 76 | ## ESLint + React 77 | 78 | We need to give ESLint a hand to get it to recognize React and not yell about React not being used. Right now it thinks we're importing React and not using because it doesn't know what to do with React. Let's help it. 79 | 80 | Run this: `npm install -D eslint-plugin-import@2.26.0 eslint-plugin-jsx-a11y@6.6.1 eslint-plugin-react@7.31.8` 81 | 82 | Update your .eslintrc.json to: 83 | 84 | ```javascript 85 | { 86 | "extends": [ 87 | "eslint:recommended", 88 | "plugin:import/errors", 89 | "plugin:react/recommended", 90 | "plugin:jsx-a11y/recommended", 91 | "prettier" 92 | ], 93 | "rules": { 94 | "react/prop-types": 0, 95 | "react/react-in-jsx-scope": 0 96 | }, 97 | "plugins": ["react", "import", "jsx-a11y"], 98 | "parserOptions": { 99 | "ecmaVersion": 2022, 100 | "sourceType": "module", 101 | "ecmaFeatures": { 102 | "jsx": true 103 | } 104 | }, 105 | "env": { 106 | "es6": true, 107 | "browser": true, 108 | "node": true 109 | }, 110 | "settings": { 111 | "react": { 112 | "version": "detect" 113 | }, 114 | "import/resolver": { 115 | "node": { 116 | "extensions": [".js", ".jsx"] 117 | } 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | > In previous versions of this course, we had to extend `prettier/react` as well as `prettier`. [As of version 8 of this plugin][prettier-react] it's all rolled into the `prettier` config. 124 | 125 | This is a little more complicated config than I used in previous versions of the workshop but this is what I use in my personal projects and what I'd recommend to you. In previous versions of this workshop, I used [airbnb][airbnb] and [standard][standard]. Feel free to check those out; I now find both of them a bit too prescriptive. Linting is a very opinionated subject, so feel free to explore what you like. 126 | 127 | This particular configuration has a lot of rules to help you quickly catch common bugs but otherwise leaves you to write code how you want. 128 | 129 | - The import plugin helps ESLint catch commons bugs around imports, exports, and modules in general 130 | - jsx-a11y catches many bugs around accessibility that can accidentally arise using React, like not having an `alt` attribute on an `img` tag. 131 | - react is mostly common React bugs like not calling one of your props children. 132 | - `eslint-plugin-react` now requires you to inform of it what version of React you're using. We're telling it here to look at the package.json to figure it out. 133 | - `"react/react-in-jsx-scope": 0` is new since you used to have to import React everywhere but now with the recent revision of React you don't need to. 134 | - Prop types allow you to add types to a component's props at runtime. In general if you're interested in doing that just use TypeScript. 135 | - We need to set the import plugin to look for both js and jsx extensions or else it won't resolve imports for us. 136 | 137 | Now your project should pass lint. 138 | 139 | > 🏁 [Click here to see the state of the project up until now: 03-jsx][step] 140 | 141 | [airbnb]: https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb 142 | [standard]: https://standardjs.com/ 143 | [step]: https://github.com/btholt/citr-v8-project/tree/master/03-jsx 144 | [prettier-react]: https://github.com/prettier/eslint-config-prettier#installation 145 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/C-effects.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "useEffect is a critical hook for React, allowing developers to do asynchronous actions like making HTTP requests" 3 | --- 4 | 5 | We have enough to start making some requests now. We want the app to request an initial set of pets on initial load of the page. So let's make that happen using a special hook called `useEffect`. `useEffect` allows you to say do a render of this component first so the user can see _something_ then as soon as the render is done, _then_ do something (the something here being an effect). In our case, we want the user to see our UI first then we want to make a request to the API so we can initialize a list of pets. 6 | 7 | Add this to SearchParams.jsx: 8 | 9 | ```javascript 10 | // change import at top 11 | import { useEffect, useState } from "react"; 12 | import Pet from "./Pet"; 13 | 14 | // add to the other useStates inside component at top 15 | const [pets, setPets] = useState([]); 16 | 17 | // add inside component, beneath all the `useState` setup 18 | useEffect(() => { 19 | requestPets(); 20 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 21 | 22 | async function requestPets() { 23 | const res = await fetch( 24 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 25 | ); 26 | const json = await res.json(); 27 | 28 | setPets(json.pets); 29 | } 30 | 31 | // in jsx, under form, inside the larger div 32 | { 33 | pets.map((pet) => ( 34 | 35 | )) 36 | } 37 | ``` 38 | 39 | - We're taking advantage of closures here that if we define the requestPets function _inside_ of the render that it will have access to that scope and can use all the hooks there. 40 | - We could have actually put requestPets inside of the effect but we're going to use it again here in a sec with the submit button. 41 | - the `[]` at the end of the useEffect is where you declare your data dependencies. React wants to know _when_ to run that effect again. You don't give it data dependencies, it assumes any time any hook changes that you should run the effect again. This is bad because that would mean any time setPets gets called it'd re-run render and all the hooks again. See a problem there? It'd run infinitely since requestPets calls setPets. 42 | - You can instead provide which hooks to watch for changes for. In our case, we actually only want it to run once, on creation of the component, and then to not run that effect again. (we'll do searching later via clicking the submit button) You can accomplish this only-run-on-creation by providing an empty array. 43 | - The `// eslint-disable-line react-hooks/exhaustive-deps` tells eslint to shut up about this one run on this one line. Why? Because eslint tries to help you with you the data dependencies rule by watching for anything that _could_ change. In this case, in theory the function could change but we know it's not important. You'll end up silencing this rule a fair bit. 44 | - We could solve this by moving the requestPets function inside the effect and rely on React to call the fetch upon effect. This strategy would mean any time a user types in the location (and thus calls setState on the location) it'd request from the API. This could work for you but for now we'll retain more control and just do it on submit events. It's all about managing when those effects go off. For now we just want this effect run once at the beginning and then not again. 45 | - At the end, we gather take the pets we got back from the API and create Pet components out of each of them. 46 | 47 | > 🏁 [Click here to see the state of the project up until now: 05-useeffect][step] 48 | 49 | [step]: https://github.com/btholt/citr-v8-project/tree/master/05-useeffect 50 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/D-custom-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "You can even make your own hooks! Brian shows how to extract logic out of a component to share a hook across components!" 3 | --- 4 | 5 | For now, we're going to make a custom hook of our own. Just like `useState` is a hook, there are a few others like `useEffect` (which we'll use in this lesson), `useReducer` (for doing Redux-like reducers), `useRefs` (for when you need to have programmatic access to a DOM node), and `useContext` (for using React's context which we'll do shortly as well.) But like React hooks, we can use these hooks to make our re-usable hooks. 6 | 7 | We need a list of breeds based on which animal is selected. In general this would be nice to request _once_ and if a user returns later to the same animal, that we would have some cache of that. We could implement in the component (and in general I probably would, this is overengineering it for just one use) but let's make a custom hook for it. 8 | 9 | Make a new file called `useBreedList.js` in src and put this in it. 10 | 11 | > .js or .jsx here, doesn't matter. It doesn't technically need JSX but I'm also fine with the "I don't want to think about it" approach to it as well. 12 | 13 | ```javascript 14 | import { useState, useEffect } from "react"; 15 | 16 | const localCache = {}; 17 | 18 | export default function useBreedList(animal) { 19 | const [breedList, setBreedList] = useState([]); 20 | const [status, setStatus] = useState("unloaded"); 21 | 22 | useEffect(() => { 23 | if (!animal) { 24 | setBreedList([]); 25 | } else if (localCache[animal]) { 26 | setBreedList(localCache[animal]); 27 | } else { 28 | requestBreedList(); 29 | } 30 | 31 | async function requestBreedList() { 32 | setBreedList([]); 33 | setStatus("loading"); 34 | const res = await fetch( 35 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 36 | ); 37 | const json = await res.json(); 38 | localCache[animal] = json.breeds || []; 39 | setBreedList(localCache[animal]); 40 | setStatus("loaded"); 41 | } 42 | }, [animal]); 43 | 44 | return [breedList, status]; 45 | } 46 | ``` 47 | 48 | - We're using hooks inside of our custom hook. I can't think of a custom hook you would make that wouldn't make use of other hooks. 49 | - We're returning two things back to the consumer of this custom hook: a list of breeds (including an empty list when it doesn't have anything in it) and an enumerated type of the status of the hook: unloaded, loading, or loaded. We won't be using the enum today but this is how I'd design it later if I wanted to throw up a nice loading graphic while breeds were being loaded. 50 | - We're tossing in `localCache` so if it loads once, it won't have to reload the same API call in the same session. You could take this further by sticking it in local storage or we could be more intelligent about ETags. 51 | 52 | Head over to SearchParams.jsx and put this in there. 53 | 54 | ```javascript 55 | import useBreedList from "./useBreedList"; 56 | 57 | // replace `const breeds = [];` 58 | const [breeds] = useBreedList(animal); 59 | ``` 60 | 61 | That should be enough! Now you should have breeds being populated every time you change animal! (Do note we haven't implemented the submit button yet though.) 62 | 63 | > 🏁 [Click here to see the state of the project up until now: 06-custom-hooks][step] 64 | 65 | [step]: https://github.com/btholt/citr-v8-project/tree/master/06-custom-hooks 66 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/E-handling-user-input.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian shows how to wire up your app to work with users interacting with the site." 3 | --- 4 | 5 | We've seen one way to handle async code in React: with effects. This is most useful when you need to be reactive to your data changing or when you're setting up or tearing down a component. Sometimes you just need to respond to someone pressing a button. This isn't hard to accomplish either. Let's make it so whenever someone either hits enter or clicks the button it searches for animals. We can do this by listening for submit events on the form. Let's go do that now. In SearchParams.js: 6 | 7 | ```javascript 8 | // replace
9 | { 11 | e.preventDefault(); 12 | requestPets(); 13 | }} 14 | > 15 | ``` 16 | 17 | Now you should be able to see the network request go out whenever you submit the form. 18 | 19 | This course isn't going into all the ways of handling user interactions in JavaScript. You can register handlers for things mouse leave, mouse enter, key up, key down, and can even handle stuff like copy and paste events, focus, blur, etc. [Here's a list of them from the React docs][docs]. 20 | 21 | [docs]: https://reactjs.org/docs/events.html#supported-events 22 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/F-component-composition.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "One component should do one thing. Brian shows you how to break down bigger components into smaller components." 3 | --- 4 | 5 | Our SearchParams component is getting pretty big and doing a lot of heavy lifting. This is against the React way: in general we want small-ish (use your best judgment but lean towards smaller when you have a choice) components that do one thing. When we start having a ballooning component like we do here, take your larger component and break it down into smaller components. 6 | 7 | In general I find two reasons to break a component into smaller components: reusability and organization. When you want to use the same component in multiple places (e.g. a button, a tool tip, etc.) then it's helpful to have one component to maintain, test, use, etc. 8 | 9 | Other times it can be useful to break concepts down into smaller concepts to make a component read better. For example, if we put all the logic for this entire page into one component, it would become pretty hard to read and manage. By breaking it down we can make each component easier to understand when you read it and thus maintain. 10 | 11 | Let's make a better display for our Pets components. Make a new file called Results.jsx. 12 | 13 | ```javascript 14 | import Pet from "./Pet"; 15 | 16 | const Results = ({ pets }) => { 17 | return ( 18 |
19 | {!pets.length ? ( 20 |

No Pets Found

21 | ) : ( 22 | pets.map((pet) => { 23 | return ( 24 | 33 | ); 34 | }) 35 | )} 36 |
37 | ); 38 | }; 39 | 40 | export default Results; 41 | ``` 42 | 43 | Now go back to SearchParams.jsx and put this: 44 | 45 | ```javascript 46 | // at top, replace import from Pet.jsx 47 | import Results from "./Results"; 48 | 49 | // under
, still inside the div, replace { pets.map ... } 50 | ; 51 | ``` 52 | 53 | Now you should be able to make request and see those propagated to the DOM! Pretty great! 54 | 55 | Let's go make Pet.jsx look decent: 56 | 57 | ```javascript 58 | const Pet = (props) => { 59 | const { name, animal, breed, images, location, id } = props; 60 | 61 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 62 | if (images.length) { 63 | hero = images[0]; 64 | } 65 | 66 | return ( 67 | 68 |
69 | {name} 70 |
71 |
72 |

{name}

73 |

{`${animal} — ${breed} — ${location}`}

74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Pet; 80 | ``` 81 | 82 | Looks much better! The links don't go anywhere yet but we'll get there. We don't have a good loading experience yet though. Right now we just seem unresponsive. Using a new tool to React called Suspense we can make the DOM rendering wait until we finish loading our data, show a loader, and then once it finishes we can resume rendering it. 83 | 84 | The previous way you would have done this is just keep track of a boolean loading state as a hook and then conditionally shown UI based about that boolean. _Now_, with suspense, you throw a promise from within that component and React will catch that promise and _suspend_ that rendering and show a fallback while it waits for that rendering to complete. We'll see that in a bit. 85 | 86 | > 🏁 [Click here to see the state of the project up until now: 07-component-composition][step] 87 | 88 | [step]: https://github.com/btholt/citr-v8-project/tree/master/07-component-composition 89 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/G-react-dev-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "An essential tool in any React developer's toolbox is the official React Dev Tools extension. Brian shows you how to install and use them." 3 | --- 4 | 5 | React has some really great tools to enhance your developer experience. We'll go over a few of them here. 6 | 7 | ## `NODE_ENV=development` 8 | 9 | React already has a lot of developer conveniences built into it out of the box. What's better is that they automatically strip it out when you compile your code for production. 10 | 11 | So how do you get the debugging conveniences then? Well, if you're using Vite.js, it will compile your development server with an environment variable of `NODE_ENV=development` and then when you run `vite build` it will automatically change that to `NODE_ENV=production` which is how all the extra weight gets stripped out. 12 | 13 | Why is it important that we strip the debug stuff out? The dev bundle of React is quite a bit bigger and quite a bit slower than the production build. Make sure you're compiling with the correct environmental variables or your users will suffer. 14 | 15 | ## Strict Mode 16 | 17 | React has a new strict mode. If you wrap your app in `` it will give you additional warnings about things you shouldn't be doing. I'm not teaching you anything that would trip warnings from `React.StrictMode` but it's good to keep your team in line and not using legacy features or things that will be soon be deprecated. 18 | 19 | We are not going to add StrictMode to our app. One thing that StrictMode does with React 18 is run twice the initialization functions of your apps to check to see if they are indeed truly stateless. While in theory this is a good thing to assure, it's wasteful to ongoing continually do as it will double invoke your APIs while you're in development which is not something we want do now. Feel free to add to your app but we are not going to today. 20 | 21 | ## Dev Tools 22 | 23 | React has wonderful dev tools that the core team maintains. They're available for both Chromium-based browsers and Firefox. They let you do several things like explore your React app like a DOM tree, modify state and props on the fly to test things out, tease out performance problems, and programtically manipulate components. Definitely worth downloading now. [See here][dev-tools] for links. 24 | 25 | [dev-tools]: https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-devtools-profiler 26 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "book" 3 | } -------------------------------------------------------------------------------- /lessons/05-react-capabilities/A-react-router.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "react-router is phenomenal tool is that allows you to manage browser navigation state in a very React way." 3 | --- 4 | 5 | > In previous versions of this course, I've taught various versions of [React Router][rr] as well as [Reach Router][reach]. It's all written by the same folks (same great people behind [Remix][remix]) but suffice to say it's a bit of a moving target. It's great software and you'll be well served by any of them. This course uses React Router v6. 6 | 7 | React Router is by far the most popular client side router in the React community. It is mature, being used by big companies, and battle tested at large scales. It also has a lot of really cool capabilities, some of which we'll examine here. 8 | 9 | What we want to do now is to add a second page to our application: a Details page where you can out more about each animal. 10 | 11 | Let's quickly make a second page so we can switch between the two. Make a file called Details.jsx. 12 | 13 | ```javascript 14 | const Details = () => { 15 | return

hi!

; 16 | }; 17 | 18 | export default Details; 19 | ``` 20 | 21 | Now the Results page is its own component. This makes it easy to bring in the router to be able to switch pages. Run `npm install react-router-dom@6.4.1`. 22 | 23 | Now we have two pages and the router available. Let's go make it ready to switch between the two. In `App.jsx`: 24 | 25 | ```javascript 26 | // at top 27 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 28 | import Details from "./Details"; 29 | 30 | // replace and

Adopt Me!

31 | 32 |

Adopt Me!

33 | 34 | } /> 35 | } /> 36 | 37 |
38 | ``` 39 | 40 | > If you're upset about the element prop vs children, [read their rationale here][element] 41 | 42 | Now we have the router working (but still have an issue)! Try navigating to [http://localhost:5173/]() and then to [http://localhost:5173/details/1](). Both should work … sort of! 43 | 44 | - React Router has a ton of features that we're not going to explain here. The docs do a great job. 45 | - The `:id` part is a variable. In [http://localhost:5173/details/1](), `1` would be the variable. 46 | - The killer feature of React Router is that it's really accessible. It manages things like focus so you don't have to. Pretty great. 47 | - If you're familiar with previous versions of React Router, quite a bit changed here. Gone is Switch, exact, and a load of other things. They broke a lot of things to bring in the best of Reach Router. It can be a slog to keep up with react-router's changes, but at the end of the day it's hard to argue they aren't improving quite a bit. 48 | - Previously this would have rendered both pages on the Details page because technically both pages match on a regex level. This changed with v6. Now it uses the same scoring system as Reach Router to pick the best route for each path. It's so much easier. I have yet to have any issue with it. 49 | 50 | So now let's make the two pages link to each other. Go to Pet.jsx. 51 | 52 | ```javascript 53 | // at top 54 | import { Link } from "react-router-dom"; 55 | 56 | // change wrapping 57 | 58 | […] 59 | 60 | ``` 61 | 62 | Why did we change this? Didn't the `` work? It did but with a flaw: every link you clicked would end up in the browser navigating to a whole new page which means React would totally reload your entire app all over again. With `` it can intercept this and just handle that all client-side. Much faster and a better user experience. 63 | 64 | Now each result is a link to a details page! And that id is being passed as a prop to Details. Try replacing the return of Details with: 65 | 66 | ```javascript 67 | import { useParams } from "react-router-dom"; 68 | 69 | const Details = () => { 70 | const { id } = useParams(); 71 | return

{id}

; 72 | }; 73 | 74 | export default Details; 75 | ``` 76 | 77 | The `useParams` hook is how you get params from React Router. It used to be through the props but now they prefer this API. 78 | 79 | Let's make the Adopt Me! header clickable too in App.jsx: 80 | 81 | ```javascript 82 | // import Link too 83 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 84 | 85 | // replace h1 86 |
87 | Adopt Me! 88 |
89 | ``` 90 | 91 | > If you're getting a useHref error, make sure your `header` is _inside_ `` 92 | 93 | Now if you click the header, it'll take you back to the Results page. Cool. Now let's round out the Details page. 94 | 95 | > 🏁 [Click here to see the state of the project up until now: 08-react-router][step] 96 | 97 | [rr]: https://reacttraining.com/react-router/ 98 | [reach]: https://reach.tech/router/ 99 | [rf]: https://twitter.com/ryanflorence 100 | [step]: https://github.com/btholt/citr-v8-project/tree/master/08-react-router 101 | [remix]: https://remix.run 102 | [element]: https://reactrouter.com/en/6.6.1/upgrading/v5#advantages-of-route-element 103 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/C-uncontrolled-forms.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | We also want to move SearchParms to use react-query but we have a problem: if we just plug `location` directly into the cache key as-is we will make a new request on _every_ keystroke of the user. That may be what you want but it's not the behavior we had before and for now we want to stay with that. 6 | 7 | But what about animal and breed? We _do_ want to react to animal changing on the breed drop down. So how we do handle that too? 8 | 9 | We're going to mix an uncontrolled form in with tracking _just_ animal as a controlled input. 10 | 11 | Before we get too far, let's split out requestPets into a file called fetchSearch.js 12 | 13 | ```javascript 14 | async function fetchSearch({ queryKey }) { 15 | const { animal, location, breed } = queryKey[1]; 16 | const res = await fetch( 17 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 18 | ); 19 | 20 | if (!res.ok) 21 | throw new Error(`pet search not okay: ${animal}, ${location}, ${breed}`); 22 | 23 | return res.json(); 24 | } 25 | 26 | export default fetchSearch; 27 | ``` 28 | 29 | From there let's go modify SearchParams.jsx 30 | 31 | ```javascript 32 | // at top 33 | // remove useEffect import from 'react' import 34 | import { useQuery } from "@tanstack/react-query"; 35 | import fetchSearch from "./fetchSearch"; 36 | 37 | // inside render, at top 38 | // delete location and breed useState calls 39 | const [requestParams, setRequestParams] = useState({ 40 | location: "", 41 | animal: "", 42 | breed: "", 43 | }); 44 | 45 | const results = useQuery(["search", requestParams], fetchSearch); 46 | const pets = results?.data?.pets ?? []; 47 | 48 | // delete useEffect 49 | 50 | // delete requestPets 51 | 52 | // replace the form submit function body 53 | e.preventDefault(); 54 | const formData = new FormData(e.target); 55 | const obj = { 56 | animal: formData.get("animal") ?? "", 57 | breed: formData.get("breed") ?? "", 58 | location: formData.get("location") ?? "", 59 | }; 60 | setRequestParams(obj); 61 | 62 | // remove onChange and onBlur functions for breed and location select and input 63 | // remove value={location} / value={animal} / value={breed} from three input / selects 64 | // add name="animal" / name="location" / name="breed" to the three input / selects 65 | ``` 66 | 67 | - Notice how much faster it is going back-and-forth from one search query and back to another. The cache for this is fast and easy to use 68 | - We no longer have _any_ useEffect calls in our code. This won't always be the case but it's a nice thing to have. useEffect calls are a lot more difficult to get your head around. Where you have alternatives (like react-query) I suggest avoiding useEffect calls and offload that async code to a smart library like react-query 69 | - We're now doing an uncontrolled form with React (which unless you have specific validation needs or dependencies like we do with animal, I suggest you always do). We don't have to have verbose two-way data binding code to control the form, we can just wait until a users submits, gather the data, and ship it off to the API 70 | - We do have a controlled input on animal to properly have it determine the useBreedList animal. But we're not using the controlled input to submit the form, we're just using the form event anyway 71 | 72 | There you go! Now our app is totally powered by react-query and no more effects in the App. I showed you how to write useEffect because it is a critical tool to know how to use with React and central to it, but I wanted to show you how to write it and then refactor it out later. 73 | 74 | > 🏁 [Click here to see the state of the project up until now: 10-uncontrolled-forms][step] 75 | 76 | [step]: https://github.com/btholt/citr-v8-project/tree/master/10-uncontrolled-forms 77 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/D-class-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Class components work a little different from hooks in terms of marshalling state. Brian teaches you how to manage your state using setState and life cycle methods." 3 | --- 4 | 5 | Let's make a nice photo carousel of the pictures for the animal now. We're going to do this using class components which is the "older" way of doing React. It's still fairly common to write components this way and still supported (i.e. not deprecated) so it's useful for you to know how to do it. 6 | 7 | Make a new file called Carousel.jsx: 8 | 9 | ```javascript 10 | import { Component } from "react"; 11 | 12 | class Carousel extends Component { 13 | state = { 14 | active: 0, 15 | }; 16 | 17 | static defaultProps = { 18 | images: ["http://pets-images.dev-apis.com/pets/none.jpg"], 19 | }; 20 | 21 | render() { 22 | const { active } = this.state; 23 | const { images } = this.props; 24 | return ( 25 |
26 | animal 27 |
28 | {images.map((photo, index) => ( 29 | // eslint-disable-next-line 30 | animal thumbnail 36 | ))} 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default Carousel; 44 | ``` 45 | 46 | - Every class component extends `React.Component`. Every class component must have a render method that returns some sort of JSX / markup / call to `React.createElement`. 47 | - We used to have a `constructor` function to set initial state. Now with class properties we can skip that. If you want to see how that looked, [check out v7 of this course][v7] 48 | - Notice instead of getting props via parameters and state via `useState` we're getting it from the instance variables `this.state` and `this.props`. This is how it works with class components. Neither one will you mutate directly. 49 | - `this.state` is the mutable state of the component (like useState). You'll use `this.setState` to mutate it (don't modify it directly.) 50 | - `this.props` comes from the parent component, similar to parameter given to the render functions that we pull props out of. 51 | - We also set `defaultProps` in the case that someone uses this component without providing it with props. This allows us to always assume that the photos prop is going to be an array instead of having to do a bunch of "if this thing exists" logic. 52 | 53 | ## Lifecycle methods 54 | 55 | Class components have lifecycle methods. These for the most part are what `useEffect` does for function components. They're for doing things like making API calls, starting and ending transitions/animations, debugging, and other things like that. We don't need to use any here, but let's look at a few of the most common ones 56 | 57 | - `constructor` isn't necessarily a _React_ lifecylce method but we use it like one. It's where you do things that need to happen before the first render. Generally it's where you set the initial state. 58 | - `componentDidMount` is a function that's called after the first rendering is completed. This pretty similar to a `useEffect` call that only calls the first time. This is typically where you want to do data fetching. It doesn't have to be async; we just made it async here to make the data fetching easy. 59 | - `componentDidUpdate` is called after your state is updated. If you're doing something like Typeahead where you're making reactive requests to an API based on user input, this would be an ideal place to do it. 60 | - `componentWillUnmount` is typically a place for cleanup. Let's say you had to write a component to integrate with jQuery (I've had to write this, multiple times), this is where you'd clean up those references (like unattaching from DOM nodes and deleting them) so you don't leak memory. This method is invoked whenever a component is about to be destroyed. 61 | 62 | This class doesn't cover all the lifecycle methods but you can imagine having different timings for different capabilities of a component can be useful. For example, if you have a set of props that come in and you need to filter those props before you display them, you can use `getDerivedStateFromProps`. Or if you need to react to your component being removed from the DOM (like if you're subscribing to an API and you need to dispose of the subscription) you can use `componentWillUnmount`. 63 | 64 | There are lots more you can check out in [the React docs here][docs]. 65 | 66 | Add the Carousel component to the Detail page. 67 | 68 | ```javascript 69 | // import at top 70 | import Carousel from "./Carousel"; 71 | 72 | // first component inside div.details 73 | ; 74 | ``` 75 | 76 | Let's make it so we can react to someone changing the photo on the carousel. 77 | 78 | ```javascript 79 | // add event listener 80 | handleIndexClick = event => { 81 | this.setState({ 82 | active: +event.target.dataset.index 83 | }); 84 | }; 85 | 86 | // above smaller img 87 | // eslint-disable-next-line 88 | 89 | // add to img 90 | onClick={this.handleIndexClick} 91 | data-index={index} 92 | ``` 93 | 94 | - This is how you handle events in React class components. If it was keyboard handler, you'd do an onChange or onKeyUp, etc. handler. 95 | - Notice that the handleIndexClick function is an arrow function. This is because we need the `this` in `handleIndexClick` to be the correct `this`. An arrow function assures that because it will be the scope of where it was defined. This is common with how to deal with event handlers with class components. 96 | - The data attribute comes back as a string. We want it to be a number, hence the `+`. 97 | - We're doing bad accessibility stuff. But this makes it a lot simpler for learning for now. But don't do this in production. 98 | 99 | > 🏁 [Click here to see the state of the project up until now: 11-class-components][step] 100 | 101 | [step]: https://github.com/btholt/citr-v8-project/tree/master/11-class-components 102 | [babel]: https://babeljs.io/ 103 | [docs]: https://reactjs.org/docs/react-component.html 104 | [v7]: https://btholt.github.io/complete-intro-to-react-v7/lessons/react-capabilities/class-components 105 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "map" 3 | } -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/A-error-boundaries.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Error boundaries allow you to catch errors coming out of a component and be able to react to that. This is great for areas where unexpected errors could arise like API calls or user generated content." 3 | --- 4 | 5 | Frequently there's errors with APIs with malformatted or otherwise weird data. Let's be defensive about this because we still want to use this API but we can't control when we get errors. We're going to use a feature called `componentDidCatch` to handle this. This is something you can't do with hooks so if you needed this sort of functionality you'd have to use a class component. 6 | 7 | This will also catch 404s on our API if someone give it an invalid ID! 8 | 9 | A component can only catch errors in its children, so that's important to keep in mind. It cannot catch its own errors. Let's go make a wrapper to use on Details.js. Make a new file called ErrorBoundary.jsx 10 | 11 | ```javascript 12 | // mostly code from reactjs.org/docs/error-boundaries.html 13 | import { Component } from "react"; 14 | import { Link } from "react-router-dom"; 15 | 16 | class ErrorBoundary extends Component { 17 | state = { hasError: false }; 18 | static getDerivedStateFromError() { 19 | return { hasError: true }; 20 | } 21 | componentDidCatch(error, info) { 22 | console.error("ErrorBoundary caught an error", error, info); 23 | } 24 | render() { 25 | if (this.state.hasError) { 26 | return ( 27 |

28 | There was an error with this listing. Click here{" "} 29 | to back to the home page. 30 |

31 | ); 32 | } 33 | 34 | return this.props.children; 35 | } 36 | } 37 | 38 | export default ErrorBoundary; 39 | ``` 40 | 41 | - Now anything that is a child of this component will have errors caught here. Think of this like a catch block from try/catch. 42 | - A static method is one that can be called on the constructor. You'd call this method like this: `ErrorBoundary.getDerivedStateFromError(error)`. This method must be static. 43 | - If you want to call an error logging service, `componentDidCatch` would be an amazing place to do that. I can recommend [Sentry][sentry] and [TrackJS][trackjs]. 44 | 45 | Let's go make Details use it. Go to Details.jsx 46 | 47 | ```javascript 48 | // add import 49 | import ErrorBoundary from "./ErrorBoundary"; 50 | 51 | // replace export 52 | export default function DetailsErrorBoundary(props) { 53 | return ( 54 | 55 |
56 | 57 | ); 58 | } 59 | ``` 60 | 61 | - Now this is totally self contained. No one rendering Details has to know that it has its own error boundary. I'll let you decide if you like this pattern or if you would have preferred doing this in App.js at the Router level. Differing opinions exist. 62 | - We totally could have made ErrorBoundary a bit more flexible and made it able to accept a component to display in cases of errors. In general I recommend the "WET" code rule (as opposed to [DRY][dry], lol): Write Everything Twice (or I even prefer Write Everything Thrice). In this case, we have one use case for this component, so I won't spend the extra time to make it flexible. If I used it again, I'd make it work for both of those use cases, but not _every_ use case. On the third or fourth time, I'd then go back and invest the time to make it flexible. 63 | 64 | > 🏁 [Click here to see the state of the project up until now: 12-error-boundaries][step] 65 | 66 | [step]: https://github.com/btholt/citr-v8-project/tree/master/12-error-boundaries 67 | [sentry]: https://sentry.io/ 68 | [trackjs]: https://trackjs.com/ 69 | [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself 70 | -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/B-portals-and-refs.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Portals allow you to render to a place outside of a component from within a component. Think of a contextual nav bar or side nav." 3 | --- 4 | 5 | Another nice feature React is something called a Portal. You can think of the portal as a separate mount point (the actual DOM node which your app is put into) for your React app. A common use case for this is going to be doing modals. You'll have your normal app with its normal mount point and then you can also put different content into a separate mount point (like a modal or a contextual nav bar) directly from a component. Pretty cool! 6 | 7 | First thing, let's go into index.html and add a separate mount point: 8 | 9 | ```html 10 | 11 | 12 | ``` 13 | 14 | This where the modal will actually be mounted whenever we render to this portal. Totally separate from our app root. 15 | 16 | Next create a file called Modal.jsx: 17 | 18 | ```javascript 19 | import React, { useEffect, useRef } from "react"; 20 | import { createPortal } from "react-dom"; 21 | 22 | const Modal = ({ children }) => { 23 | const elRef = useRef(null); 24 | if (!elRef.current) { 25 | elRef.current = document.createElement("div"); 26 | } 27 | 28 | useEffect(() => { 29 | const modalRoot = document.getElementById("modal"); 30 | modalRoot.appendChild(elRef.current); 31 | return () => modalRoot.removeChild(elRef.current); 32 | }, []); 33 | 34 | return createPortal(
{children}
, elRef.current); 35 | }; 36 | 37 | export default Modal; 38 | ``` 39 | 40 | - This will mount a div and mount inside of the portal whenever the Modal is rendered and then _remove_ itself whenever it's unrendered. 41 | - We're using the feature of `useEffect` that if you need to clean up after you're done (we need to remove the div once the Modal is no longer being rendered) you can return a function inside of `useEffect` that cleans up. 42 | - We're also using a ref here via the hook `useRef`. Refs are like instance variables for function components. Whereas on a class you'd say `this.myVar` to refer to an instance variable, with function components you can use refs. They're containers of state that live outside a function's closure state which means anytime I refer to `elRef.current`, it's **always referring to the same element**. This is different from a `useState` call because the variable returned from that `useState` call will **always refer to the state of the variable when that function was called.** It seems like a weird hair to split but it's important when you have async calls and effects because that variable can change and nearly always you want the `useState` variable, but with something like a portal it's important we always refer to the same DOM div; we don't want a lot of portals. 43 | - Down at the bottom we use React's `createPortal` to pass the children (whatever you put inside ``) to the portal div. 44 | 45 | Now go to Details.jsx and add: 46 | 47 | ```javascript 48 | // at the top 49 | import { useState } from "react"; 50 | import Modal from "./Modal"; 51 | 52 | // add showModal 53 | const [showModal, setShowModal] = useState(false); 54 | 55 | // add onClick to ; 57 | 58 | // below description 59 | { 60 | showModal ? ( 61 | 62 |
63 |

Would you like to adopt {pet.name}?

64 |
65 | 66 | 67 |
68 |
69 |
70 | ) : null; // you have to remove this semi-colon, my auto-formatter adds it back if I delete it 71 | } 72 | ``` 73 | 74 | Notice that despite we're rendering a whole different part of the DOM we're still referencing the state in Details.jsx. This is the magic of Portals. You can use state but render in different parts of the DOM. Imagine a sidebar with contextual navigation. Or a contextual footer. It opens up a lot of cool possibilities. React Router has some cool features built into that take advantage of this as well. 75 | 76 | We'll add a "yes" function here in the next lesson 77 | 78 | That's it! That's how you make a modal using a portal in React. This used to be significantly more difficult to do but with portals it became trivial. The nice thing about portals is that despite the actual elements being in different DOM trees, these are in the same React trees, so you can do event bubbling up from the modal. Some times this is useful if you want to make your Modal more flexible (like we did.) 79 | 80 | > 🏁 [Click here to see the state of the project up until now: 13-portals-and-refs][step] 81 | 82 | [portal]: https://reactjs.org/docs/portals.html 83 | [step]: https://github.com/btholt/citr-v8-project/tree/master/13-portals-and-refs 84 | -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/C-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Context allows you to share state across an entire app. While a powerful feature it has drawbacks which Brian discusses here." 3 | --- 4 | 5 | What is context? Context is like state, but instead of being confined to a component, it's global to your application. It's application-level state. This is dangerous. Avoid using context until you _have_ to use it. One of React's primary benefit is it makes the flow of data obvious by being explicit. This can make it cumbersome at times but it's worth it because your code stays legible and understandable. Things like context obscure it. 6 | 7 | Context (mostly) replaces Redux. Well, typically. It fills the same need as Redux. I really can't see why you would need to use both. Use one or the other. 8 | 9 | Again, this is a contrived example. What we're doing here is overkill and should be accomplished via React's normal patterns. A better example would be something like a user's logged-in information. But let's check out what this looks like with theme. 10 | 11 | Imagine if a user adopts an animal that we want to show that user that pet as they navigate around the site. A sort of shopping cart experience of sorts. In our case, we're going to make it if a user clicks "Yes" on the modal to adopt a pet, it's going to show that pet's picture at the top of the search facets as a shopping cart of sorts. A shopping cart would be a valid use case for context. 12 | 13 | Make a new file called AdoptedPetContext.js: 14 | 15 | ```javascript 16 | import { createContext } from "react"; 17 | 18 | const AdoptedPetContext = createContext(); 19 | 20 | export default AdoptedPetContext; 21 | ``` 22 | 23 | `createContext` is a function that returns an object with two React components in it: a Provider and a Consumer. A Provider is how you scope where a context goes. A context will only be available inside of the Provider. You only need to do this once. 24 | 25 | A Consumer is how you consume from the above provider. A Consumer accepts a function as a child and gives it the context which you can use. We won't be using the Consumer directly: a function called `useContext` will do that for us. 26 | 27 | The object provided to context is the default state it uses when it can find no Provider above it, useful if there's a chance no provider will be there and for testing. It's also useful for TypeScript because TypeScript will enforce these types. We'll look more at this in Intermediate React when looking at TypeScript. 28 | 29 | You do not have to use context with hooks; [see v4 of this course][v4] if you want to see how to do it without hooks. 30 | 31 | Let's go to App.jsx 32 | 33 | ```javascript 34 | // import useState and AdoptedPetContext 35 | import { useState } from "react"; 36 | import AdoptedPetContext from "./AdoptedPetContext"; 37 | 38 | // top of App function body 39 | const adoptedPet = useState(null); 40 | 41 | // wrap the rest of the app inside of BrowserRouter 42 | […]; 43 | ``` 44 | 45 | - We're going to use the `useState` hook because the adopted pet is actually going to be kept track of like any other piece of state: it's not any different. You can think of context like a wormhole: whatever you chuck in one side of the wormhole is going to come out the other side. 46 | - You have to wrap your app in a `Provider`. This is the mechanism by which React will notify the higher components to re-render whenever our context changes. Then whatever you pass into the value prop (we passed in the complete hook, the value and updater pair) will exit on the other side whenever we ask for it. 47 | - Note that the adopted pet will only be available _inside_ of this provider. So if we only wrapped the `
` route with the Provider, that context would not be available inside of ``. 48 | - Side note: if your context is _read only_ (meaning it will _never change_) you actually can skip wrapping your app in a Provider. 49 | 50 | Next let's go to Details.jsx: 51 | 52 | ```javascript 53 | // import at top 54 | import { useContext, useState } from "react"; 55 | import { useNavigate, useParams } from "react-router-dom"; 56 | import AdoptedPetContext from "./AdoptedPetContext"; 57 | 58 | // top of Details function body 59 | const navigate = useNavigate(); 60 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 61 | 62 | // replace Yes button 63 | ; 71 | ``` 72 | 73 | - Here we are setting the "adopted" pet to using the setter of the hook when the user clicks yes. 74 | - Upon clicking yes we want to set the adopted pet via the context and then navigate the user to the home page so they can see what they did. The `useNavigate` hook gives back a `navigate` function from react-router-dom that allows you to navigate to a different page. 75 | - We don't have to set the modal to hide. The whole page is about to unmount so no need to. 76 | 77 | Let's go do this in SearchParams.jsx 78 | 79 | ```javascript 80 | // import 81 | import { useContext, useState } from "react"; 82 | import AdoptedPetContext from "./AdoptedPetContext"; 83 | 84 | // add at top of SearchParams render function 85 | const [adoptedPet] = useContext(AdoptedPetContext); 86 | 87 | // just inside
88 | { 89 | adoptedPet ? ( 90 |
91 | {adoptedPet.name} 92 |
93 | ) : null; // you have to remove this semi-colon, my auto-formatter adds it back if I delete it 94 | } 95 | ``` 96 | 97 | - We're consuming the state from SearchParams. But you can imagine doing this in several places in the app. You could head off to a "finalize adoption" page and have all that data ready. 98 | - Consuming context from a class component is more verbose. [See v7 of this course][v7] to see that if you're interested. Instead of doing a pet to adopt, we did theming. 99 | 100 | That's it for context! Something like shopping carts, theming, or logged-in user data would be perfect for context. It's for app-level data. Everything else should be boring-ol' state. 101 | 102 | > 🏁 [Click here to see the state of the project up until now: 14-context][step] 103 | 104 | [step]: https://github.com/btholt/citr-v8-project/tree/master/14-context 105 | [v4]: https://btholt.github.io/complete-intro-to-react-v4/context 106 | [v7]: https://btholt.github.io/complete-intro-to-react-v7/lessons/special-case-react-tools/context 107 | -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "bolt" 3 | } -------------------------------------------------------------------------------- /lessons/07-end-of-intro/A-conclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "A quick note and congratulations from Brian for completing the Complete Intro to React v7!" 3 | --- 4 | 5 | This concludes our intro to React! You know now nearly every feature of the React core library (there are a few more but are rarely needed.) What makes React wonderful is not only the core library itself but the ecosystem around. Indeed, the ecosystem around it is as much a reason to learn React as React itself. 6 | 7 | The modules following this one are considered to be optional modules: you don't need to know all of these; you can just pick the ones you need for your project or what interests you the most and leave the others behind. For the most part these modules will be self-contained: you don't need to complete all the optional modules to understand what's going on. Some _will_ use the code you built in the previous modules here but feel free to clone the complete project and work from there. 8 | 9 | Good job getting this far and good luck on the next modules! 10 | -------------------------------------------------------------------------------- /lessons/07-end-of-intro/B-ways-to-expand-your-app.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "If you want to improve this app and have some more practice with React, here are some ideas for you!" 3 | --- 4 | 5 | If you want to improve this app and have some more practice with React, here are some ideas for you! 6 | 7 | ## Take the Intermediate React Course 8 | 9 | Take the Intermediate course! You'll learn great things like Tailwind, how to write tests for React, TypeScript, how to use React with Node.js, code splitting, and a whole slew of other things. 10 | 11 | ## Paginate the Results 12 | 13 | Our home page doesn't paginate doesn't results. With some nice buttons, you could paginate through the various results so a user isn't stuck looking at the top ten results. `http://pets-v2.dev-apis.com/pets?animal=dog&page=1` will give you the second page of dogs (pages for this API start at 0). 14 | 15 | ## Use a Real API 16 | 17 | [Use the Petfinder API!][pf] In previous versions of this course we did actually use the Petfinder API but it was occasionally unreliable so I made the fake API you're using to make sure you could always work through the code okay. 18 | 19 | [They even have a JavaScript library!][pf-sdk] You'll have to sign up for API credentials (secret and key) on their website, install the library, and then use the library everywhere we were using `fetch()` you need to change it to `pf.animal.search()` or whatever calls. This API returns different shpae of data. Last time I checked it looks like this: 20 | 21 | ```json 22 | { 23 | "id": 44895949, 24 | "organization_id": "NOTREAL", 25 | "url": "https://www.url.to.the.animal/", 26 | "type": "Rabbit", 27 | "species": "Rabbit", 28 | "breeds": { 29 | "primary": "Mini Rex", 30 | "secondary": null, 31 | "mixed": true, 32 | "unknown": false 33 | }, 34 | "colors": { 35 | "primary": "Brown / Chocolate", 36 | "secondary": "Tan", 37 | "tertiary": null 38 | }, 39 | "age": "Adult", 40 | "gender": "Female", 41 | "size": "Small", 42 | "coat": "Short", 43 | "attributes": { 44 | "spayed_neutered": true, 45 | "house_trained": false, 46 | "declawed": null, 47 | "special_needs": false, 48 | "shots_current": false 49 | }, 50 | "environment": { 51 | "children": null, 52 | "dogs": null, 53 | "cats": null 54 | }, 55 | "tags": [], 56 | "name": "Betty", 57 | "description": "Hi my name is Betty and I am 1 year old.", 58 | "photos": [ 59 | { 60 | "small": "https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/44895949/1/?bust=1559843027&width=100", 61 | "medium": "https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/44895949/1/?bust=1559843027&width=300", 62 | "large": "https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/44895949/1/?bust=1559843027&width=600", 63 | "full": "https://dl5zpyw5k3jeb.cloudfront.net/photos/pets/44895949/1/?bust=1559843027" 64 | } 65 | ], 66 | "status": "adoptable", 67 | "published_at": "2019-06-06T17:44:29+0000", 68 | "contact": { 69 | "email": "fake@example.com", 70 | "phone": "(555) 555-5555", 71 | "address": { 72 | "address1": "Not Real SPCA", 73 | "address2": "Fake Place", 74 | "city": "Fake City", 75 | "state": "FS", 76 | "postcode": "00000", 77 | "country": "US" 78 | } 79 | }, 80 | "_links": { 81 | "self": { 82 | "href": "/v2/animals/44895949" 83 | }, 84 | "type": { 85 | "href": "/v2/types/rabbit" 86 | }, 87 | "organization": { 88 | "href": "/v2/organizations/NOTREAL" 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | Once you've done all this, your code will actually be populated with real animals!! 95 | 96 | ## Deploy your Code 97 | 98 | You should deploy your code to the cloud and tweet it at me! Great options for places for you to deploy include: 99 | 100 | - [Netlify][netlify] 101 | - [Vercel][vercel] 102 | - [Azure Static Web Apps][swa] 103 | - [Google Firebase][gcp] 104 | - [AWS Amplify][aws] 105 | 106 | ## Make your app themeable via context 107 | 108 | Make a dark mode! Make a party mode! Add animiations! This would be great when paired with the Tailwind section from Intermediate React. 109 | 110 | ## Add a Navigation Bar 111 | 112 | Right now we don't have a great navigation story for our little pet finding app. Add a navigation bar at the top so users can easily navigate our site. 113 | 114 | ## Play with other tools 115 | 116 | I showed you how to use Vite but consider trying one of the newer build systems like [Parcel], [Snowpack], [ESBuild], or any of the others. You could also use one of the popular mainstays like [Webpack][webpack] or [Rollup][rollup]. 117 | 118 | ## Let me know 119 | 120 | Please! Let's share all the great apps we make here so we can provide inspirations for others and get some high fives on the cool work we do. Tweet it out and let me know. 121 | 122 | [pf]: https://www.petfinder.com/developers/ 123 | [pf-sdk]: https://github.com/petfinder-com/petfinder-js-sdk 124 | [swa]: https://azure.microsoft.com/en-us/services/app-service/static/ 125 | [gcp]: https://firebase.google.com/ 126 | [aws]: https://aws.amazon.com/amplify/ 127 | [netlify]: https://www.netlify.com/ 128 | [vercel]: https://vercel.com/ 129 | [parcel]: https://parceljs.org/ 130 | [snowpack]: https://www.snowpack.dev/ 131 | [esbuild]: https://esbuild.github.io/ 132 | [webpack]: https://webpack.js.org/ 133 | [rollup]: https://www.rollupjs.org/guide/en/ 134 | -------------------------------------------------------------------------------- /lessons/07-end-of-intro/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "stopwatch" 3 | } -------------------------------------------------------------------------------- /lessons/08-intermediate-react-v5/A-welcome-to-intermediate-react-v5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Welcome to Intermediate React v5" 3 | description: "Brian introduces you to Intermediate React v5 and explains how the course is structured." 4 | --- 5 | 6 | Welcome to Intermediate React v5 as taught by Brian Holt 7 | 8 | If you haven't yet, read the [intro page][intro] as it explains well who I am, why I'm teaching this course, and what my background is. 9 | 10 | This course is structured a bit different than the Complete Intro to React v8. Whereas the Intro class is a project based class and the whole class builds on one continuous project, this one takes a bunch of unrelated concepts and teaches them as little modules. Feel free to skip modules that don't apply to you. 11 | 12 | All project files are here: [citr-v8-project][citr]. This is where you'll find all the various solutions and steps to different parts of the project. 13 | 14 | Specifically, most modules will work on a fresh copy of [the 14-context folder][project]. This folder is the completed project from the Complete Intro to React v8. After each module, we'll restart with a fresh copy of this folder (i.e. the modules **do not** build on each other.) 15 | 16 | Alright! Let's have some fun with Intermdiate React v5! 17 | 18 | [intro]: https://btholt.github.io/complete-intro-to-react-v6/intro 19 | [citr]: https://github.com/btholt/citr-v8-project/ 20 | [project]: https://github.com/btholt/citr-v8-project/tree/master/14-context 21 | -------------------------------------------------------------------------------- /lessons/08-intermediate-react-v5/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Intermediate React v5" 3 | } -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/A-useref.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useRef" 3 | description: "" 4 | --- 5 | 6 | > We're going to do this in StackBlitz but this code runs locally too. It's using Vite.js as well. Link to [the GitHub repo is here][gh] and [the StackBlitz is here][sb]. One big note: the examples _only work_ in the embed version in Chromium-based browsers (e.g. Chrome, Edge, Vivaldi, etc.). It does work in Firefox but only if you go to the whole site and not rely on the embed version. 7 | 8 | ### [Link directly to StackBlitz][ref] 9 | 10 | 11 | 12 | Refs can be used for a variety of purposes. One particular use for them is if you need to interact with _the actual DOM_ (as opposed to the React virtualization of it) directly. This is pretty rare and really only when you need to derive measurements from the DOM (like width) or you're using an external library and it needs a real DOM node to interact with. 13 | 14 | In our example, let's integrate [Three.js][three] with React. Three.js is a library that allows you to do 3D graphics in the browser and has its own runtime outside of React. React never guarantees that a DOM node isn't going to re-render at any given time and in general this is a good thing: we don't have to care about actually updating the DOM: React does it for us. However it's a problem with Three.js: we need to insert a DOM node directly into a DOM element (which React would control.) That's where the `ref` is useful: it allows to get an actual hold on the DOM node underneath so we can interact with it. 15 | 16 | ## Why memo? 17 | 18 | React is very fast at re-rendering and 99.999% of the time you never have to worry when React re-renders, just that your view is a function of your state. 19 | 20 | The .001% of times you care is when you have something that is either very performance sensitive and you want to have a tighter grip on the performance or something like our Three.js app running in it (where re-rendering it will destroy and re-create a pretty expensive thing to re-render). It also looks bad because it'll reset the animation. 21 | 22 | `React.memo` tells React "as long as the parameters being passed into this component don't change, _do not re-render it ever_. You might be tempted to do this on every component but believe me, _don't_. Things will not re-render when you expect them to and you will forget you memoized them. Only use memo where you need to. 23 | 24 | [three]: https://threejs.org/ 25 | [sb]: https://stackblitz.com/edit/ir5 26 | [gh]: https://github.com/btholt/react-hooks-examples-v5 27 | [ref]: https://stackblitz.com/edit/ir5?&view=both&file=src/routes/UseRef.jsx&hideExplorer=1&initialPath=/useRef 28 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/B-usereducer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useReducer" 3 | description: "" 4 | --- 5 | 6 | ### [Link directly to StackBlitz][ref] 7 | 8 | 9 | 10 | I'm going to assume you're familiar with Redux. If not, there's a brief section on it [here](https://redux.js.org/introduction/getting-started/). `useReducer` allows us to do Redux-style reducers but inside a hook. Here, instead of having a bunch of functions to update our various properties, we have one reducer that handles all the updates based on an action type. This is a preferable approach if you have complex state updates or if you have a situation like this: all of the state updates are very similar so it makes sense to contain all of them in one function. 11 | 12 | In this one, we are using [hsl color][hsl] to make it so we have text that is always _somewhat_ readable against its background. I use a very rudimentary algorithm to do this, [there is an algorithm that is better at it][lab], but I wanted to keep it simple. We basically just make sure one color is 180º away in hues and 50% different in lightness (no need to modify saturation). It produces decent, but not perfect results. 13 | 14 | [ref]: https://stackblitz.com/edit/ir5?view=both&file=src/routes/UseReducer.jsx&hideExplorer=1&initialPath=/useReducer 15 | [lab]: https://en.wikipedia.org/wiki/CIELAB_color_space 16 | [hsl]: https://en.wikipedia.org/wiki/HSL_and_HSV 17 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/C-usememo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useMemo" 3 | description: "useMemo memoizes expensive function calls so they only are re-evaluated when needed." 4 | --- 5 | 6 | ### [Link directly to StackBlitz][ref] 7 | 8 | 9 | 10 | `useMemo` and `useCallback` are performance optimizations. Use them only when you already have a performance problem instead of pre-emptively. It adds unnecessary complexity otherwise. 11 | 12 | `useMemo` memoizes expensive function calls so they only are re-evaluated when needed. I put in the [fibonacci sequence][fibonacci] in its recursive style to simulate this. All you need to know is that once you're calling `fibonacci` with 30+ it gets quite computationally expensive and not something you want to do unnecessarily as it will cause pauses and jank. It will now only call `fibonacci` if count changes and will just use the previous memoized answer if it hasn't changed. 13 | 14 | If we didn't have the `useMemo` call, everytime the ball moved it'd unnecessarily recalculate the answer of `fibonacci`, but because we did use `useMemo` it will only calculate it when `count` has changed. 15 | 16 | Feel free to try to remove `useMemo` and see what happens. It'll cause the ball animation to be pretty janky. 17 | 18 | [ref]: https://stackblitz.com/edit/ir5?view=both&file=src/routes/UseMemo.jsx&hideExplorer=1&initialPath=/useMemo 19 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/D-usecallback.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useCallback" 3 | description: "useCallback is quite similar and indeed it's implemented with the same mechanisms as useMemo except it's a callback instead of a value" 4 | --- 5 | 6 | ### [Link directly to StackBlitz][ref] 7 | 8 | 9 | 10 | `useCallback` is quite similar and indeed it's implemented with the same mechanisms as `useMemo`. Our goal is that `UseRefComponent` (which is the same Three.js component from the useRef example) only re-renders whenever it absolutely must. Typically whenever React detects a change higher-up in an app, it re-renders everything underneath it. This normally isn't a big deal because React is quite fast at normal things. However you can run into performance issues sometimes where some components are bad to re-render without reason. 11 | 12 | In this case, we're using the feature of React called `React.memo`. This is similar to `PureComponent` where a component will do a simple check on its props to see if they've changed and if not it will not re-render this component (or its children, which can bite you.) `React.memo` provides this functionality for function components. Given that, we need to make sure that the function itself given to `UseRefComponent` is the _same_ function every time. We can use `useCallback` to make sure that React is handing the exact same (i.e. `===` and not just `==`) to `UseRefComponent` every time so it passes its `React.memo` check every single time. Now it'll only re-render if we give it a different parameter. 13 | 14 | Try removing the useCallback call and see the Three.js app crash. 15 | 16 | [ref]: https://stackblitz.com/edit/ir5?view=both&file=src/routes/UseCallback.jsx&hideExplorer=1&initialPath=/useCallback 17 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/E-uselayouteffect.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useLayoutEffect" 3 | description: "" 4 | --- 5 | 6 | ### [Link directly to StackBlitz][ref] 7 | 8 | 9 | 10 | `useLayoutEffect` is almost the same as `useEffect` except that it's synchronous to render as opposed to scheduled like `useEffect` is. If you're migrating from a class component to a hooks-using function component, this can be helpful too because `useLayoutEffect` runs at the same time as `componentDidMount` and `componentDidUpdate` whereas `useEffect` is scheduled after. This should be a temporary fix. 11 | 12 | The only time you _should_ be using `useLayoutEffect` is to measure DOM nodes for things like animations. In the example, I measure the textarea after every time you click on it (the onClick is to force a re-render.) This means you're running render twice but it's also necessary to be able to capture the correct measurments. 13 | 14 | If you make the `useLayoutEffect` into a `useEffect` it will have a janky re-render where it'll flash before it renders correctly. This is exactly why we need `useLayoutEffect`. 15 | 16 | > Note if you drag "off" the textarea and let go of the mouse it won't measure the textarea. This is because when you click and hold something and then drag off of it, it doesn't trigger a click event. That makes sense, right? When you click something by mistake, what do you do? You drag off of it and let go. Same princple here. 17 | 18 | [ref]: https://stackblitz.com/edit/ir5?view=both&file=src/routes/UseLayoutEffect.jsx&hideExplorer=1&initialPath=/useLayoutEffect 19 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/F-useid.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useId" 3 | description: "useId allows you to generate consistent IDs across renders" 4 | --- 5 | 6 | ### [Link directly to StackBlitz][ref] 7 | 8 | 9 | 10 | A new hook for version 18 of React is `useId`. Frequently in React you need unique identifiers to associate two objects together. An example of this would be making sure a label and an input are associated together by the `htmlFor` attribute. 11 | 12 | Previously you could maintain some sort of unique counter that was tracked across renders. With concurrent React and batching in version 18 that's no longer possible. `useId` will give you a consistent via a hook so that they can always be the same. 13 | 14 | This is useful for the thing we see above: we have a label which needs a `for` attribute that corresponds to an input. We would either need to use some piece of data/parameter that we'd pass into the component that would serve as the key or we can use this hook to give it a unique ID. 15 | 16 | If you need multiple IDs in the same component just do `{id}-name`, `{id}-address`, ``{id}-number`, etc. No need to call `useId` multiple times. 17 | 18 | This is safe across server-side renders and client-side. 19 | 20 | [ref]: https://stackblitz.com/edit/ir5?view=both&file=src/routes/UseId.jsx&hideExplorer=1&initialPath=/useId 21 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/G-others.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | We didn't cover quite everything that React offers in terms of hooks. I tried to keep it to things that I think you will use on a regular basis and leave the extra-special cased ones for you learn on a need-to-know basis. These ones I have _never had to use professionally_ and I imagine many of you will be in my boat. However I am going to list them out here for you. 6 | 7 | ## useImperativeHandle 8 | 9 | Imagine you make a super fancy input component as part of your design system. Imagine that a _parent element_ (i.e. the component that renders the fancy input) that needs to call `focus()` on the fancy input because of a validation error. 10 | 11 | This is what `useImperativeHandle` is for. It allows a child component to expose a method (I used focus as an example but could be anything). You pass in a ref from `useRef` to the child component, it uses that ref to pass back methods to the parent. If you make libraries or design systems, this is useful. Otherwise there are easier ways to do this. 12 | 13 | ## useDebugValue 14 | 15 | This is useful for people make custom hooks (like [we did in the Complete Intro to React][citr]). If you want your hook to expose a custom debugging value to the React Dev Tools, useDebugValue allows you to do this. I still haven't a good reason to do this so I don't teach it anymore. 16 | 17 | ## useDeferredValue 18 | 19 | This one and `useTransition` center around low priority updates. A good example of these is type-ahead suggestions. Type-ahead is not super important, and often a user is typing fast enough that lots of suggestions are getting thrown away as they type. It therefore is a low priority UI update and we should not lock up the entire UI trying to render low priority work. 20 | 21 | This is what `useDeferredValue` allows you to do. It allows you identify data which would cause a re-render as "this can be interrupted, if you have something else happen while this is trying to compute, do that other stuff first and then come back to this." 22 | 23 | This requires a lot of cognitive burden to wrap your mind around the "what" and "when" of your app and so I never find myself reaching for it. 24 | 25 | ## useTransition 26 | 27 | Likewise to `useDeferredValue`, it allows you to set up "low priority" updates. `useTransition` gives you back a function to start a transition that can be interrupted if something higher priority comes up, like a user clicking somewhere. After you start that transition, it will give you back a boolean `isCurrentlyTransitioning` flag that will allow you to show a spinner while this transition is being delayed. Once React has cleared everything out and gotten to the low priority transition, everything will settle into a normal state. 28 | 29 | Like above, this creates some indirection in how your app renders and I don't choose to use this very frequently. If you have some low priority things to render at the bottom of your page that are expensive to render (think like a comment section at the bottom of an article) then this would be a good case to use that. But for now I'd advise not using these until you really, truly have a problem that these tools are a fit for. These were built for Facebook problems and most of us don't have Facebook problems. 30 | 31 | ## useSyncExternalStore 32 | 33 | This hook was made to sync with external libraries (like Redux, Mobx, etc.) Even the React docs say this isn't really made for app devs but mostly for library devs. 34 | 35 | ## useInsertionEffect 36 | 37 | Like above, this hook was made for use with libraries. In this case, an insertion effect occurs _before_ rendering (as opposed to effect and layout effect which both happen _just after_ rendering.) Because of this, it's more limited in what it can access (e.g. it can't use refs.) 38 | 39 | Mostly insertion effects are for CSS-in-JS libraries like Emotion and styled-components. You as an app dev should not really have a use for it. 40 | 41 | [citr]: /lessons/core-react-concepts/custom-hooks 42 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "search" 3 | } -------------------------------------------------------------------------------- /lessons/10-tailwindcss/A-css-and-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian teaches you to set up the latest hotness in CSS for large scale projects, Tailwind CSS." 3 | title: "CSS and React" 4 | --- 5 | 6 | There are many ways to do CSS with React. Even just in this course I've done several ways with [style-components][sc] and [emotion][emotion]. Both of those are fantastic pieces of software and could definitely still be used today. As is the case when I teach this course I try to cover the latest material and what I think is the current state-of-the-art of the industry and today I think that is [TailwindCSS][tailwind]. 7 | 8 | Both style-components and emotion are libraries that execute in the JavaScript layer. They bring your CSS into your JavaScript. This allows you all the power of JavaScript to manipulate styles using JavaScript. 9 | 10 | Tailwind however is a different approach to this. And it bears mentioning that Tailwind isn't tied to React at all (whereas styled-components is and emotion mostly is.) Everything I'm showing you here is just incidentally using React (though Tailwind is particularly popular amongst React devs.) 11 | 12 | Let's get it set up. Run this: 13 | 14 | ```bash 15 | npm i -D tailwindcss@3.1.8 postcss@8.4.18 autoprefixer@10.4.12 16 | ``` 17 | 18 | - Under the hood, Vite processes all your CSS with PostCSS with the autoprefixer plugin. This works like Babel: it means you can write modern code and it'll make it backwards compatible with older browsers. Since we're modifying the PostCSS config (like we did with Babel earlier in this project in the Intro part) we have to give it the whole config now. 19 | 20 | Okay, now let's get our Tailwind project going. 21 | 22 | ```bash 23 | npx tailwindcss init -p 24 | ``` 25 | 26 | Like `tsc init` for TypeScript, this will spit out a basic starting config in tailwind.config.js. Should look like 27 | 28 | ```javascript 29 | /** @type {import('tailwindcss').Config} */ 30 | module.exports = { 31 | content: ["./src/**/*.{js,ts,jsx,tsx,html}"], 32 | theme: { 33 | extend: {}, 34 | }, 35 | plugins: [], 36 | }; 37 | ``` 38 | 39 | Now, let's go and replace contents of our `style.css` file with the following: 40 | 41 | ```css 42 | @tailwind base; 43 | @tailwind components; 44 | @tailwind utilities; 45 | ``` 46 | 47 | > If you're seeing Visual Studio Code give you a warning about unknown at rules in your style.css and it bothers you, open settings, search for `css.lint.unknownAtRules` and set that to ignore. 48 | 49 | This is how we include all the things we need from Tailwind. This is what allows Tailwind to bootstrap and only include the CSS you need to make your app run. 50 | 51 | > There's a great Visual Studio Code extension you should install here: [Tailwind CSS IntelliSense][tw]. 52 | 53 | Lastly, the `-p` of the bash command we ran earlier created a PostCSS config, `postcss.config.js`, for us and it should already look like this: 54 | 55 | ```javascript 56 | module.exports = { 57 | plugins: { 58 | tailwindcss: {}, 59 | autoprefixer: {}, 60 | }, 61 | }; 62 | ``` 63 | 64 | Now if you run your app you should see the React app (and all the functionality should work) but it won't have any style. We're going to quickly restyle this whole app to show you how great Tailwind is and how quickly it lets you go. 65 | 66 | [tw]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss 67 | [sc]: https://btholt.github.io/complete-intro-to-react/ 68 | [emotion]: https://btholt.github.io/complete-intro-to-react-v5/emotion 69 | [tailwind]: https://tailwindcss.com/docs 70 | -------------------------------------------------------------------------------- /lessons/10-tailwindcss/B-tailwind-basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Tailwind CSS works differently than you may expect for styling your page. Brian starts to dig into it with you." 3 | --- 4 | 5 | I'm going to need you to suspend everything you know about CSS best practices for this section. At the start this going to feel gross and weird. But stick with me. I initially had similar feelings towards React too. 6 | 7 | We are not going to be writing _any_ CSS (well, one little bit but THAT'S IT.) Tailwind is all about just using tiny utility classes and then having that be all the CSS you ever need. Let's see something _super basic_. 8 | 9 | > There are old class names from the previous CSS styling we had. Feel free to delete them or leave them. It doesn't matter. I haphazardly deleted them as I overwrote them with new class names. 10 | 11 | In App.jsx, put this: 12 | 13 | ```javascript 14 | // the outer div that wraps 15 |
21 | […] 22 |
23 | ``` 24 | 25 | - The `p-0` and `m-0` is what Tailwind is a lot of: putting a lot of tiny utility classes on HTML elements. In this case: we're making it so the encapsulating div has zero padding and zero margin. If we wanted it to have a little of either, it'd `m-1` or `p-1`. There's \*-1 through 12 and then there it's more a random increase with 12, 14, 16, 20, 24, 28, 32, 36, 40, etc. all the way up to 96. There's also `-m-1` for _negative_ margins. There's also mt, ml, mr, mb for top, left, right, bottom and mx for left and right and my for top and bottom (these all apply to p as well.) 26 | - We do have to apply the background image via styles. You'll find you'll occasionally need to do it for things that Tailwind doesn't do (like URLs) but for the most part you shouldn't need to. 27 | 28 | Let's do the whole header now. 29 | 30 | ```javascript 31 |
32 | 33 | Adopt Me! 34 | 35 |
36 | ``` 37 | 38 | - That's more what you'll see! Long class strings. I imagine some of you are upset looking at this. To be honest it's still strange to me. But we're also skinning a whole app with _zero_ CSS so it's a pretty compelling experience. 39 | - Like p and m, we have w and h. `w-1` would have a tiny width. `w-full` is width: 100%. 40 | - `bg-gradient-to-b from-yellow-400 via-orange-500 to-red-500` is a gradient just using classes. `bg-gradient-to-b` says it goes from the top to bottom (you can do -to-l, -to-r, or -to-t as well.) The from is the start. The via is a middle stop, and the to is the end. 41 | - The yellow-400 is a yellow color and the 400 is the _lightness_ of it. 50 is nearly white, 900 is as dark as the color gets. 42 | - You can set your own colors via the theme but the default ones are really good. 43 | - `text-6xl` is a really big text size. They use the sizes sm, md, lg, xl, 2xl, etc. up to 9xl. 44 | - `text-center` will do `text-align: center`. 45 | - `hover:` is how we do hover, focus, disabled, etc. It takes whatever is on the right and only applies it only when that state is true. (note: disabled doesn't work without some magic in our PostCSS 7 compat layer. We'll do that in a bit.) 46 | - Note: `` from react-router-dom will pass styles and classes down to the resulting `
` for you. 47 | 48 | Let's hop over to SearchResults.jsx (we're only doing SearchParams, I'll leave it to you to fix Details) 49 | 50 | ```javascript 51 |
52 | { 55 | e.preventDefault(); 56 | const formData = new FormData(e.target); 57 | const obj = { 58 | animal: formData.get("animal") ?? "", 59 | breed: formData.get("breed") ?? "", 60 | location: formData.get("location") ?? "", 61 | }; 62 | setRequestParams(obj); 63 | }} 64 | > 65 | […] 66 | 67 |
68 | ``` 69 | 70 | - `rounded-lg` is a "large" rounding of the corners i.e. border-radius. 71 | - `shadow-lg` is a "large" box shadow. 72 | - `flex` makes the display mode flex. `flex-col` makes it columns. `justify-center` makes it justify-content center. `items-center` makes it `align-items: center`. Net result is that you have centered horizontally and vertically items in a vertical direction. 73 | -------------------------------------------------------------------------------- /lessons/10-tailwindcss/C-tailwind-plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Tailwind CSS has the ability to add plugins to augment its functionality. Brian helps you add the most common plugin, the one for forms." 3 | --- 4 | 5 | Our inputs look really gross. We could write our own components (basically reusable CSS classes, what a novel idea) but we're just going to use the good ones that Tailwind provides out of the box. 6 | 7 | Run `npm install -D @tailwindcss/forms@0.5.3`. 8 | 9 | Put this into your tailwind.config.js 10 | 11 | ```javascript 12 | // replace plugins 13 | plugins: [require("@tailwindcss/forms")], 14 | ``` 15 | 16 | This will apply a bunch of default styles for all of our basic form elements. Tailwind has a pretty great plugin ecosystem. One of my favorites is the aspect-ratio one. CSS doesn't currently have a backwards compatible way of doing aspect ratios (e.g. keep this image in a square ratio) and this plugin makes a primitive that you can use like that. Super cool. 17 | 18 | Notice our location input still looks bad. With this plugin they (probably wisely) require you to add `type="text"` to the the input so they can have a good selector for it. So please go add that now to your text input. 19 | 20 | Let's finish making SearchParams looks nice. 21 | 22 | To each of the selects and inputs, add `className="w-60 mb-5 block"` so they have a nice uniform look. 23 | 24 | To the breed selector, we want it to be grayed out when it's not available to use. 25 | 26 | Now add `className="w-60 mb-5 block disabled:opacity-50"` to the breed ` { 36 | setAnimal(e.target.value as Animal); 37 | }} 38 | onBlur={(e) => { 39 | setAnimal(e.target.value as Animal); 40 | }} 41 | > 42 | […] 43 | 44 | ``` 45 | 46 | - We had to switch from `e.target` to `e.currentTarget` because I guess while target works it's not technically guaranteed to be on a submit event even though it always is 🤷‍♂️ 47 | - Working with the DOM with TypeScript can get annoying because there's a lot of legacy pseudo types that we never had to care about. Technically `formData.get` gives us back a `FormDataEntryValue` type and not a string but when you use it like we were it implicitly called `toString`. Now we have to do it explictly. 48 | - We need to type values as they come out of the DOM. There's no way for TypeScript to understand what goes into the DOM and what comes back out so we have to be explicit as it goes in and out. 49 | -------------------------------------------------------------------------------- /lessons/13-typescript/K-refactor-results.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian quickly converts Results.tsx" 3 | --- 4 | 5 | Now let's go do Results.tsx 6 | 7 | ```tsx 8 | import { Pet as PetType } from "./APIResponsesTypes"; 9 | import Pet from "./Pet"; 10 | 11 | const Results = ({ pets }: { pets: PetType[] }) => { … } 12 | ``` 13 | 14 | - Admittedly I could have named the Pet component and the Pet interface differently (and this is where calling it IPet could have been useful) but it's good for you to see how to handle a collision like this. Just use as to import it as a different name. 15 | - We could have made an interface with the props and then used that, but if you want to be lazy and put it directly in there it works too. 16 | -------------------------------------------------------------------------------- /lessons/13-typescript/M-refactor-app.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Brian quickly converts App.tsx and wraps up" 3 | --- 4 | 5 | Lastly, let's do App.tsx. 6 | 7 | First, we'll need to import our Pet interface: 8 | 9 | ```tsx 10 | import { Pet } from "./APIResponsesTypes"; 11 | ``` 12 | 13 | Then we can use it to check our App component's state: 14 | 15 | ```tsx 16 | // replace useState 17 | const adoptedPet = useState(null as Pet | null); 18 | 19 | // under container DOM query at the end 20 | if (!container) { 21 | throw new Error("no container to render to"); 22 | } 23 | ``` 24 | 25 | Make the last change to `AdoptedPetContext.ts`: 26 | 27 | ```tsx 28 | // replace with 29 | const AdoptedPetContext = createContext<[Pet | null, (adoptedPet: Pet) => void]>([ 30 | ``` 31 | 32 | Just a few changes to 1. let TS know that null could be a Pet. and 2. to defend against a DOM without a container to render to. 33 | 34 | Last thing: open `index.html` and change the link from `App.js` to `App.tsx` and then you should be good to go! 35 | 36 | This probably felt burdensome to do. In fact, it is. I had a difficult time writing this! Converting existing JS codebasees to TypeScript necessitates a certain amount of writing and rewriting to get all the type signatures in a place that the compiler can verify everything. Be cautious before you call for your team to rewrite. 37 | 38 | However, now that we're playing TypeScript land, this code would be joyous to work on. Visual Studio Code will autocomplete for you. TypeScript will _instantly_ let you know when you've made a mistake. You can launch new code with higher certainty that you haven't created run time errors. This all comes at the cost of taking longer to write. Ask yourself if that's a trade-off you're willing to make: if you're a tiny startup that may not happen. If you're as large as Microsoft, maybe! It's a trade-off like all things are. It is a question you should answer before you start a new code base: should we type check? 39 | 40 | Last thing, let's add a type check to our package.json just in case someone isn't using a type checking editor. Add `"typecheck": "tsc --noEmit"` to your package.json. This is also useful CI scenarios. 41 | 42 | Congrats! You finished TypeScript. 43 | 44 | > 🏁 [Click here to see the state of the project up until now: typescript-4][step] 45 | 46 | [step]: https://github.com/btholt/citr-v8-project/tree/master/typescript-4 47 | -------------------------------------------------------------------------------- /lessons/13-typescript/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "pencil-ruler", 3 | "title": "TypeScript" 4 | } -------------------------------------------------------------------------------- /lessons/14-redux/B-more-app-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Okay, let's make another page use Redux, our SearchParams.jsx 6 | 7 | This is a bit of a contrived example so stick with me here. Let's say we have the following product requirements: 8 | 9 | - When a user searches for something, then clicks on a pet, then clicks back, we want to show the same search results 10 | - We still want to leave our search params form as uncontrolled components 11 | 12 | So how would we do that? We need something that's going to have survive state changes between page loads. Redux is perfect for that sort of app state. Let's see how we'd do that. 13 | 14 | Start with a searchParamsSlice.js 15 | 16 | ```javascript 17 | import { createSlice } from "@reduxjs/toolkit"; 18 | 19 | export const searchParamsSlice = createSlice({ 20 | name: "searchParams", 21 | initialState: { 22 | value: { 23 | location: "", 24 | breed: "", 25 | animal: "", 26 | }, 27 | }, 28 | reducers: { 29 | all: (state, action) => { 30 | state.value = action.payload; 31 | }, 32 | }, 33 | }); 34 | 35 | export const { all } = searchParamsSlice.actions; 36 | 37 | export default searchParamsSlice.reducer; 38 | ``` 39 | 40 | You could have an individual reducer for each of location, breed, and animal but we don't need that now. Right now the only place we set those (the form submit) we do it all at once. So this is good as is. 41 | 42 | In store.js 43 | 44 | ```javascript 45 | // at top 46 | import searchParams from "./searchParamsSlice"; 47 | 48 | // inside reducers 49 | searchParams, 50 | ``` 51 | 52 | In SearchParams.jsx 53 | 54 | ```javascript 55 | // at top 56 | import { useSelector, useDispatch } from "react-redux"; // add dispatch 57 | import { all } from "./searchParamsSlice"; 58 | 59 | // with other hooks 60 | const dispatch = useDispatch(); 61 | const adoptedPet = useSelector((state) => state.adoptedPet.value); 62 | const searchParams = useSelector((state) => state.searchParams.value); 63 | const results = useQuery(["search", searchParams], fetchSearch); // replace requestParams 64 | 65 | // replace setRequestParams in form submit 66 | dispatch(all(obj)); 67 | ``` 68 | 69 | Not too bad, right? Now if you back and forth the app state is preserved between page loads. The form isn't reflecting it because we left the form uncontrolled. As an exercise you could go back and make it a controlled form so that would change too. 70 | -------------------------------------------------------------------------------- /lessons/14-redux/C-rtk-query.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | title: "RTK Query" 4 | --- 5 | 6 | Very similar to @tanstack/react-query that we saw in Complete Intro, there is a Redux Toolkit Query (always abbreviated as RTK Query). It works extremely similarly to react-query but with a Redux twist on it. If you're using Redux, I recommend you use RTK Query instead of react-query. Likewise I wouldn't introduce RTK to your app _just_ to use RTK Query. Just know both are similar and are wonderful to work with. 7 | 8 | So we're going to replace all of our uses of react-query with RTK Query. 9 | 10 | Make a file called petApiService.js. In there put: 11 | 12 | ```javascript 13 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 14 | 15 | export const petApi = createApi({ 16 | reducerPath: "petApi", 17 | baseQuery: fetchBaseQuery({ baseUrl: "http://pets-v2.dev-apis.com" }), 18 | endpoints: (builder) => ({ 19 | getPet: builder.query({ 20 | query: (id) => ({ url: "pets", params: { id } }), 21 | transformResponse: (response) => response.pets[0], 22 | }), 23 | }), 24 | }); 25 | 26 | export const { useGetPetQuery } = petApi; 27 | ``` 28 | 29 | - With RTK query you build these services around base URLs. In our case, our API is all on the same path so it all works out well. You then build endpoints which have their own sort of URL builders. 30 | - We built a getPet endpoint. It takes in an ID and then uses that as a URL query parameter. So with that with an ID of 4 the URL built would be the `baseUrl` + the endpoint `url` + the params so http://pets-v2.dev-apis.com/pets?id=4 31 | - `transformResponse` is so you can extract the actual part of the response you want to keep. We just want the first pet in the pets array so we nab that. 32 | - Finally `createApi` will create a hook for you to use in your app so we're going to export that. 33 | 34 | Okay, let's go put it in store.js now 35 | 36 | ```javascript 37 | import { petApi } from "./petApiService"; // import service 38 | 39 | const store = configureStore({ 40 | reducer: { 41 | adoptedPet, 42 | searchParams, 43 | [petApi.reducerPath]: petApi.reducer, // add reducer 44 | }, 45 | middleware: (getDefaultMiddleware) => 46 | getDefaultMiddleware().concat(petApi.middleware), // add middleware 47 | }); 48 | 49 | export default store; 50 | ``` 51 | 52 | - We need to add our reducer to our store. RTK query will cache our responses directly in our Redux store for us with its generated reducers but we do need to add it to our store 53 | - The middleware isn't strictly necessary but it does allow for additional feature like caching, invalidation, refetching, etc. I just always add it. But your app with basic caching does work without the middleware. 54 | 55 | Okay! Now we're ready to use this in React! Let's head to Details.jsx 56 | 57 | ```javascript 58 | // remove useQuery import 59 | // remove fetchPet import 60 | import { useGetPetQuery } from "./petApiService"; 61 | 62 | // delete const results = useQuery(["details", id], fetchPet); 63 | 64 | const { isLoading, data: pet } = useGetPetQuery(id); 65 | 66 | if (isLoading) { // remove results. 67 | […] 68 | } 69 | ``` 70 | 71 | And now it should work!! As you can see, very similar to react-query. Let's quickly do the other requests. 72 | 73 | Back in petApiService.js 74 | 75 | ```javascript 76 | // add two endpoints 77 | endpoints: (builder) => ({ 78 | […] 79 | getBreeds: builder.query({ 80 | query: (animal) => ({ url: "breeds", params: { animal } }), 81 | transformResponse: (response) => response.breeds, 82 | }), 83 | search: builder.query({ 84 | query: ({ animal, location, breed }) => ({ 85 | url: "pets", 86 | params: { animal, location, breed }, 87 | }), 88 | transformResponse: (response) => response.pets, 89 | }), 90 | }), 91 | 92 | export const { useGetBreedsQuery, useGetPetQuery, useSearchQuery } = petApi; // add exports 93 | ``` 94 | 95 | Now to useBreedList.js 96 | 97 | ```javascript 98 | import { useGetBreedsQuery } from "./petApiService"; 99 | 100 | // delete these two 101 | // import { useQuery } from "@tanstack/react-query"; 102 | // import fetchBreedList from "./fetchBreedList"; 103 | 104 | export default function useBreedList(animal) { 105 | // delete this line 106 | // const results = useQuery(["breeds", animal], fetchBreedList); 107 | 108 | const { data: breeds, isLoading } = useGetBreedsQuery(animal, { 109 | skip: !animal, 110 | }); 111 | 112 | if (!animal) { 113 | return [[], "loaded"]; 114 | } 115 | 116 | return [breeds ?? [], isLoading ? "loading" : "loaded"]; 117 | } 118 | ``` 119 | 120 | - Very close to what we had. 121 | - We're telling the hook "hey, if there's no animal, don't fetch. Give the user back an empty array" 122 | - I'm being lazy with the isLoaded status. You could look at `isLoaded`, `isFetching`, `isError`, `isSuccess`, etc. and come up with a better system. We're not using it so I'm not working too hard on it. 123 | 124 | Last one. Head to SearchParam.jsx 125 | 126 | ```javascript 127 | // remove imports for fetchSearch and useQuery 128 | import { useSearchQuery } from "./petApiService"; 129 | 130 | // replace useQuery call 131 | let { data: pets } = useSearchQuery(searchParams); 132 | pets = pets ?? []; 133 | ``` 134 | 135 | That's it! Congrats! You're now using RTK query. Again, this is awesome if you're already in Redux land but I end up mostly using react-query because I don't use Redux as much these days. 136 | -------------------------------------------------------------------------------- /lessons/14-redux/D-redux-dev-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's quickly try the dev tools: 6 | 7 | - [Firefox][fox] 8 | - [Chrome][chrome] 9 | 10 | Download the one you're using, open up your app, and mess around the Redux tab. You can time travel, auto-generate tests, modify state, see actions, all sorts of cool stuff. Another good reason to use Redux. 11 | 12 | Hopefully you're well informed on the boons and busts of introducing Redux. It's great, just be careful. 13 | 14 | [If you want a deeper dive, check out the Frontend Masters course on Redux!][fem]. 15 | 16 | [If you want to see how to do raw Redux without RTK, check out the previous version of this course][ir4] 17 | 18 | > 🏁 [Click here to see the state of the project up until now: redux][step] 19 | 20 | [step]: https://github.com/btholt/citr-v8-project/tree/master/redux 21 | [fox]: https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/ 22 | [chrome]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en 23 | [fem]: https://frontendmasters.com/courses/redux-fundamentals/ 24 | [ir4]: https://frontendmasters.com/courses/intermediate-react-v4/redux/ 25 | -------------------------------------------------------------------------------- /lessons/14-redux/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "database" 3 | } -------------------------------------------------------------------------------- /lessons/15-testing/A-testing-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | > Please start with a fresh copy of this app: [Adopt Me!][app] 6 | 7 | This is meant to be a very brief treatise on how to do testing on React applications. This will be a brief intro on how to set up Vitest tests for the application we just created. 8 | 9 | ## Testing with Vitest 10 | 11 | [Vitest][vitest] is a test runner made by the fine folks who make Vite (as well as Vue.) The idea behind Vitest is that you already have a complete build pipeline for making an app, why should that pipeline be any different for test? It shouldn't; you want your testing environment to look as much like your app environment as possible. 12 | 13 | They designed it to be a drop-in replacement for [Jest][jest] which is what I have taught for this course since the beginning. Jest is great and still a very viable tool to use for testing, even with Vite. We're just going to use Vitest because 1. we don't have to do any more configuration and 2. 100% of what you will learn in here is going to be useful if you use Jest. Win-win. If you want to learn Jest specifically, [take a look at Intermediate React v4's testing section.][v4] 14 | 15 | Also, fun side note: [Jest is now an OpenJS project and no longer directly under Facebook][fb]. Good news for everyone. 16 | 17 | While Vitest is not using Jasmine directly, its APIs mimic Jasmine APIs (just like Jest.) 18 | 19 | Let's get going Run `npm install -D vitest@0.24.3 @testing-library/react@13.4.0 happy-dom@7.6.0`. 20 | 21 | `@testing-library/react`, formerly called `react-testing-library`, is a tool that has a bunch of convenience features that make testing React significantly easier and is now the recommended way of testing React, supplanting [Enzyme][enzyme]. Previous versions of this course teach Enzyme if you'd like to see that (though I wouldn't recommend it unless you have to.) 22 | 23 | We need to tell Vitest that we need a browser-like environment which it will fulfill via the [happy-dom][hd] package. happy-dom is a lot like jsdom but smaller, doesn't do 100% of what the browser does, and is much, much faster. 24 | 25 | Next go into your src directory and create a folder called `__tests__`. Notice that's double underscores on both sides. Why double? They borrowed it from Python where double underscores ("dunders" as I've heard them called) mean something magic happens (in essence it means the name itself has significance and something is looking for that path name exactly.) In this case, Vitest assumes all JS files in here are tests. 26 | 27 | Let's go add an npm script. In your package.json. 28 | 29 | ```json 30 | "test": "vitest" 31 | ``` 32 | 33 | > Fun trick: if you call it test, npm lets you run that command as just `npm t`. 34 | 35 | This command let's you run Jest in an interactive mode where it will re-run tests selectively as you save them. This lets you get instant feedback if your test is working or not. This is probably my favorite feature of Vitest. 36 | 37 | Okay, one little configuration to add to your vite.config.js 38 | 39 | ```javascript 40 | // add this to the config object 41 | test: { 42 | environment: "happy-dom", 43 | }, 44 | ``` 45 | 46 | Now that we've got that going, let's go write a test. 47 | 48 | [jest]: https://jestjs.io 49 | [jasmine]: https://jasmine.github.io/ 50 | [enzyme]: http://airbnb.io/enzyme/ 51 | [istanbul]: https://istanbul.js.org 52 | [res]: https://raw.githubusercontent.com/btholt/complete-intro-to-react-v5/testing/__mocks__/@frontendmasters/res.json 53 | [app]: https://github.com/btholt/citr-v8-project/tree/master/14-context 54 | [fb]: https://twitter.com/cpojer/status/1524419433938046977 55 | [hd]: https://github.com/capricorn86/happy-dom 56 | [vitest]: https://vitest.dev/ 57 | [v4]: https://frontendmasters.com/courses/intermediate-react-v4/setup-jest-testing-library/ 58 | -------------------------------------------------------------------------------- /lessons/15-testing/B-basic-react-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's write our first test for Pet.jsx. In general, here's my methodology for testing React: 6 | 7 | - Try to test functionality, not implementation. Make your tests interact with components as a user would, not as a developer would. This means you're trying to do more to think of things like "what would a user see" or "if a user clicks a button a modal comes up" rather than "make sure this state is correct" or "ensure this library is called". This isn't a rule; sometimes you need to test those things too for assurance the app is working correctly. Use your best judgment. 8 | - Every UI I've ever worked on changes a lot. Try to not unnecessarily spin your wheels on things that aren't important and are likely to change. 9 | - In general when I encounter a bug that is important for me to go back and fix, I'll write a test that would have caught that bug. Actually what I'll do is _before_ I fix it, I'll write the test that fails. That way I fix it I'll know I won't regress back there. 10 | - Ask yourself what's important about your app and spend your time testing that. Ask yourself "if a user couldn't do X then the app is worthless" sort of questions and test those more thoroughly. If a user can't change themes then it's probably not the end of the world (a11y is important) so you can spend less time testing that but if a user can't log in then the app is worthless. Test that. 11 | - Delete tests on a regular basis. Tests have a shelf life. 12 | - Fix or delete flaky tests. Bad tests are worse than no tests 13 | 14 | Okay, create a new file called `Pet.test.jsx`. This naming convention is just habit. `Pet.spec.jsx` is common too. But as long as it's in the `__tests__` directory it doesn't much matter what you call it. 15 | 16 | ```javascript 17 | import { expect, test } from "vitest"; 18 | import { render } from "@testing-library/react"; 19 | import Pet from "../Pet"; 20 | 21 | test("displays a default thumbnail", async () => { 22 | const pet = render(); 23 | 24 | const petThumbnail = await pet.findByTestId("thumbnail"); 25 | expect(petThumbnail.src).toContain("none.jpg"); 26 | pet.unmount(); 27 | }); 28 | ``` 29 | 30 | > 🚨 This doesn't work yet. That's intentional. 31 | 32 | See the `findByTestId` function? This lets us stick IDs in our code that React testing library can latch onto to test. Go into your `Pet.jsx` and add to the `` tag `data-testid="thumbnail"` to it so that your test can find it. It's advantageous to use these test IDs and decouple them from the existing CSS selector hierarchy because now it's very portable and not fragile. It's very intentional and obvious what it's supposed to do. If we moved the `` we could just move the test ID and not have to fix more code. 33 | 34 | The test doesn't pass? Oh, that's because it caught a bug! If you don't give it an images array, it just breaks. That defeats the purpose of having a default image! Let's go fix it in Pet.js. 35 | 36 | We do have to call unmount. Due to how Vitest runs test, it can cause flaky tests if we don't clean up each test after it's down. If you don't unmount you may get a `TestingLibraryElementError: Found multiple elements by: [data-testid="thumbnail"]` error. 37 | 38 | ```javascript 39 | if (images && images.length) { 40 | hero = images[0]; 41 | } 42 | ``` 43 | 44 | This doesn't work!? Why? Well, turns out react-router-dom gets upset if you try to render its components without a Router above it. We could either go mock the APIs it's expecting (gross) or we could just give it a router. Let's do that. 45 | 46 | ```javascript 47 | // at top 48 | import { StaticRouter } from "react-router-dom/server"; 49 | 50 | // replace render 51 | const pet = render( 52 | 53 | 54 | 55 | ); 56 | ``` 57 | 58 | Now it should pass! 59 | 60 | Let's add one more test case for good measure to test the non-default use case. 61 | 62 | ```javascript 63 | test("displays a non-default thumbnail", async () => { 64 | const pet = render( 65 | 66 | 67 | 68 | ); 69 | 70 | const petThumbnail = await pet.findByTestId("thumbnail"); 71 | expect(petThumbnail.src).toContain("1.jpg"); 72 | pet.unmount(); 73 | }); 74 | ``` 75 | 76 | Bam! Some easy React testing there for you. 77 | -------------------------------------------------------------------------------- /lessons/15-testing/C-testing-ui-interactions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Testing UI Interactions" 3 | description: "" 4 | --- 5 | 6 | Now we want to test some UI interaction. If a user does X then we want to verify that Y happens. We're going to dig into the Carousel. If a user clicks an thumbnail it should make the hero image change to be that image. 7 | 8 | In general I do like these kinds of tests. They tell a user story: if a user clicks a thumbnail they expect to see the hero image change to that. It's not a technical implementation but a reflection of what a user expects from you app. 9 | 10 | Go create in your `__tests__` directory a file called Carousel.test.jsx. In there put: 11 | 12 | ```javascript 13 | import { expect, test } from "vitest"; 14 | import { render } from "@testing-library/react"; 15 | import Carousel from "../Carousel"; 16 | 17 | test("lets users click on thumbnails to make them the hero", async () => { 18 | const images = ["0.jpg", "1.jpg", "2.jpg", "3.jpg"]; 19 | const carousel = render(); 20 | 21 | const hero = await carousel.findByTestId("hero"); 22 | expect(hero.src).toContain(images[0]); 23 | 24 | for (let i = 0; i < images.length; i++) { 25 | const image = images[i]; 26 | 27 | const thumb = await carousel.findByTestId(`thumbnail${i}`); 28 | await thumb.click(); 29 | 30 | expect(hero.src).toContain(image); 31 | expect(Array.from(thumb.classList)).toContain("active"); 32 | } 33 | }); 34 | ``` 35 | 36 | In Carousel.js add the following `data-testid`s. 37 | 38 | ```javascript 39 | // to the hero image 40 | data-testid="hero" 41 | 42 | // to the thumbnail 43 | data-testid={`thumbnail${index}`} 44 | ``` 45 | 46 | This is going to check first to see if you set the first image to correctly be the hero, and then check by clicking each of the thumbnails to make them the hero. The first one is intentionally "wasted" because we want to make sure that if a user clicks the active thumbnail that nothing changes. We also check to make sure that the thumbnail gets an active class so we can style it differently. 47 | 48 | This isn't a thoroughly exhaustive test but I'm fine with it here. The point to instill confidence that it mostly works. We could definitely go further (check to see if other thumbnails don't have active for example) but I think this is a good starting point. 49 | -------------------------------------------------------------------------------- /lessons/15-testing/D-testing-custom-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's say we needs tests for our custom hook, useBreedList. Testing custom hooks is a bit of a trick because they are inherently tied to the internal workings of React: they can't be called outside of a component. So how we do we get around that? We fake a component! Make a file called useBreedList.test.jsx in our `__tests__` directory. 6 | 7 | ```javascript 8 | import { expect, test } from "vitest"; 9 | import { render } from "@testing-library/react"; 10 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 11 | import useBreedList from "../useBreedList"; 12 | 13 | const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | staleTime: Infinity, 17 | cacheTime: Infinity, 18 | retry: false, 19 | }, 20 | }, 21 | }); 22 | 23 | function getBreedList(animal) { 24 | let list; 25 | 26 | function TestComponent() { 27 | list = useBreedList(animal); 28 | return null; 29 | } 30 | 31 | render( 32 | 33 | 34 | 35 | ); 36 | 37 | return list; 38 | } 39 | 40 | test("gives an empty list with no animal", async () => { 41 | const [breedList, status] = getBreedList(); 42 | expect(breedList).toHaveLength(0); 43 | expect(status).toBe("loading"); 44 | }); 45 | ``` 46 | 47 | It's a little weird to implement a fake component to test something (we're dangerously close to the line of testing implementation details) but this is essentially library code and we want to assure ourselves this code works if we use it frequently in our code base. We also have to provide for the query provider because it relies on it being there. We're giving it a `retry: false` key-value pair because we want it to fail fast instead of retrying. 48 | 49 | We can make this better though. Let's rewrite our test to look like this: 50 | 51 | ```javascript 52 | import { expect, test } from "vitest"; 53 | import { renderHook } from "@testing-library/react"; 54 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 55 | import useBreedList from "../useBreedList"; 56 | 57 | const queryClient = new QueryClient({ 58 | defaultOptions: { 59 | queries: { 60 | staleTime: Infinity, 61 | cacheTime: Infinity, 62 | retry: false, 63 | }, 64 | }, 65 | }); 66 | 67 | test("gives an empty list with no animal", async () => { 68 | const { result } = renderHook(() => useBreedList(""), { 69 | wrapper: ({ children }) => ( 70 | {children} 71 | ), 72 | }); 73 | 74 | const [breedList, status] = result.current; 75 | 76 | expect(breedList).toHaveLength(0); 77 | expect(status).toBe("loading"); 78 | }); 79 | ``` 80 | 81 | Here the helper `renderHook` abstracts away that oddity we had to do to get that hook tested. But rest assured it's doing essentially the same thing: creating a component under the hood that's running the hook lifecycle methods appropriately for you. We do still have to give it a wrapper to appropriately give it the context it needs for react-query but that's it. 82 | -------------------------------------------------------------------------------- /lessons/15-testing/E-mocks.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's write a second test for actually making a request with our custom hook, useBreedList. But we have a problem: we don't actually want to `fetch` from our API. This can be slow and cause unnecessary load on a server or unnecessary complexity of spinning up a testing API. We can instead mock the call. A mock is a fake implementation. We _could_ write our own fake fetch but a good one already exists for Vitest called vitest-fetch-mock so let's install that. Run 6 | 7 | ```bash 8 | npm install -D vitest-fetch-mock@0.2.1 9 | ``` 10 | 11 | We now need to make it so Vitest implements this mock before we run our tests. We can make it run a set up script by putting this in our vite.config.js: 12 | 13 | ```javascript 14 | // inside "test" 15 | setupFiles: ["./setupVitest.js"], 16 | ``` 17 | 18 | Then let's make a file in src called setupVitest.js. 19 | 20 | ```javascript 21 | import createFetchMock from "vitest-fetch-mock"; 22 | import { vi } from "vitest"; 23 | 24 | const fetchMock = createFetchMock(vi); 25 | fetchMock.enableMocks(); 26 | ``` 27 | 28 | Easy, right? Now it will fake all calls to fetch and we can provide fake API responses. We could provide a whole fake implementation here but let's do it in the testing code itself. If I was doing a lot of fake API calls, I might generate an [OpenAPI][openapi] spec and use that to generate a fake API but that's pretty advance. Start small and grow when you hit barriers. 29 | 30 | Okay, now go back to our useBreedList.test.js and add: 31 | 32 | ```javascript 33 | // grab waitFor 34 | import { renderHook, waitFor } from "@testing-library/react"; 35 | 36 | // add at bottom 37 | test("gives back breeds with an animal", async () => { 38 | const breeds = [ 39 | "Havanese", 40 | "Bichon Frise", 41 | "Poodle", 42 | "Maltese", 43 | "Golden Retriever", 44 | "Labrador", 45 | "Husky", 46 | ]; 47 | fetch.mockResponseOnce( 48 | JSON.stringify({ 49 | animal: "dog", 50 | breeds, 51 | }) 52 | ); 53 | const { result } = renderHook(() => useBreedList("dog"), { 54 | wrapper: ({ children }) => ( 55 | {children} 56 | ), 57 | }); 58 | 59 | await waitFor(() => expect(result.current[1]).toBe("success")); 60 | 61 | const [breedList] = result.current; 62 | expect(breedList).toEqual(breeds); 63 | }); 64 | ``` 65 | 66 | The `waitFor` allows us to sit back and wait for all of React's machinery to churn through the updates, effects, etc. until our data is ready for us to check on. And that's it! In general you should mock API calls. It will make tests run much faster and save unnecessary load on an API. 67 | 68 | [openapi]: https://swagger.io/ 69 | -------------------------------------------------------------------------------- /lessons/15-testing/G-c8.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | title: "c8" 4 | --- 5 | 6 | One last very cool trick that Vitest has built into it: [c8][c8]. c8 is a tool which tells you _how much_ of your code that you're covering with tests. Via an interactive viewer you can see what lines are and aren't covered. This used to be annoying to set up by Vitest just does it for you. 7 | 8 | Add the following command to your npm scripts: `"test:coverage": "vitest --coverage"` and go ahead run `npm run test:coverage` and open the following file in your browser: `open src/coverage/index.html`. 9 | 10 | > It will likely ask you to install a module to do this. Say yes. Note: depending on when you arrive here, you may get an error and will likely need to install specific version used. 11 | 12 | Here you can see the four files we've written tests for. One file, `fetchBreedList` is missing a line of coverage (click on the file name to see that): it's the line of reading back from the cache. That actually is a pretty important thing to cover as it could be a source of bugs (cache might as well be the French word for software bug). This can help identify gaps in your testing coverage. 13 | 14 | Lastly, add `coverage/` to your `.gitignore` since this shouldn't be checked in. 15 | 16 | ## Istanbul 17 | 18 | c8 use Node.js's built-in code coverage capabilities to run your tests which makes it significantly faster and outputs it in a way that all of [Istanbul][istanbul] / nyc's tools work with it. You can tell Vitest to use Istanbul but unless you have a very specific reason to, just use c8. 19 | 20 | > 🏁 [Click here to see the state of the project up until now: testing][step] 21 | 22 | [step]: https://github.com/btholt/citr-v8-project/tree/master/testing 23 | [istanbul]: https://istanbul.js.org/ 24 | [c8]: https://github.com/bcoe/c8 25 | -------------------------------------------------------------------------------- /lessons/15-testing/H-visual-studio-code-extension.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Visual Studio Code has an amazing VS Code extension to use with Vitest. It makes working Vitest a wonderful experience." 3 | --- 4 | 5 | [Please go install this extension][vitest-vscode]. 6 | 7 | This extension makes working with Vitest magical ✨ 8 | 9 | Some highlights: 10 | 11 | - Every time you save a file your test suite will run so you can failures faster. (this is configurable if your tests take a long time to run.) 12 | - A nice little visual indicator on the tests themselves that they past their last run. 13 | - The ability to debug individual tests. 14 | - Inline you can see test failures. You can see which test is failing and why. 15 | - A nice test explore pane on the sidebar. 16 | - You can update Vitest snapshots directly from a prompt in VS Code. 17 | - You can see Vitest snapshots directly from the test code without having to navigate to them. 18 | 19 | I'm a big fan. Now every time I work with Vitest I use it. Highly suggest installing it anytime you're near Vitest. 20 | 21 | [vitest-vscode]: https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer 22 | -------------------------------------------------------------------------------- /lessons/15-testing/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "vial" 3 | } -------------------------------------------------------------------------------- /lessons/16-end-of-intermediate/A-end-of-intermediate.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Thank you for sticking through this and hopefully you feel like you really dug into a good portion of the React ecosystem. With these additional tools in your React toolbox I feel like you're ready to take on any project that you decide to tackle. 6 | 7 | Please let me know how you liked the course and definitely [tweet at me][holtbt]! If you haven't I'd love a [star on this repo too][star]. 8 | 9 | Thanks again and I'll see you [back on Frontend Masters!][fem]! 10 | 11 | ## Next step projects 12 | 13 | - Write good tests to get 100% coverage on the whole project 14 | - Finish the migration from CSS to TailwindCSS 15 | - Combine a few of the sections. Try writing tests for TypeScript, TailwindCSS server-side rendering, etc. 16 | - Deploy your projects and tell me about it! 17 | 18 | [fem]: https://frontendmasters.com/teachers/brian-holt/ 19 | [holtbt]: https://twitter.com/holtbt 20 | [star]: https://github.com/btholt/complete-intro-to-react-v6 21 | -------------------------------------------------------------------------------- /lessons/16-end-of-intermediate/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "stopwatch" 3 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import path from "path"; 3 | 4 | const buffer = readFileSync(path.join(process.cwd(), "./course.json")); 5 | const course = JSON.parse(buffer); 6 | const BASE_URL = course?.productionBaseUrl || ""; 7 | 8 | const config = { 9 | basePath: BASE_URL, 10 | env: { 11 | BASE_URL, 12 | }, 13 | async redirects() { 14 | if (BASE_URL) { 15 | return [ 16 | { 17 | source: "/", 18 | destination: BASE_URL, 19 | basePath: false, 20 | permanent: false, 21 | }, 22 | ]; 23 | } 24 | return []; 25 | }, 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "license": "(CC-BY-NC-4.0 OR Apache-2.0)", 5 | "author": "Brian Holt ", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build && npm run csv", 9 | "export": "next build && next export && npm run csv", 10 | "start": "next start", 11 | "csv": "node csv/index.js" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-free": "^6.2.0", 15 | "gray-matter": "^4.0.3", 16 | "highlight.js": "^11.6.0", 17 | "marked": "^4.1.0", 18 | "next": "^12.3.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "title-case": "^3.0.3" 22 | }, 23 | "devDependencies": { 24 | "convert-array-to-csv": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import "@fortawesome/fontawesome-free/css/all.css"; 3 | 4 | import "highlight.js/styles/a11y-light.css"; 5 | import "../styles/variables.css"; 6 | import "../styles/footer.css"; 7 | import "../styles/courses.css"; 8 | 9 | import Layout from "../components/layout"; 10 | 11 | export default function App({ Component, pageProps }) { 12 | return ( 13 | 14 | 15 | 20 | 26 | 32 | 38 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | 4 | import { getLessons } from "../data/lesson"; 5 | 6 | import Corner from "../components/corner"; 7 | import getCourseConfig from "../data/course"; 8 | 9 | export default function Lessons({ sections }) { 10 | const courseInfo = getCourseConfig(); 11 | return ( 12 | <> 13 | 14 | {courseInfo.title} 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |

{courseInfo.title}

30 |

{courseInfo.subtitle}

31 |
32 |
33 | author image 38 |
39 |
40 |
{courseInfo.author.name}
41 |
{courseInfo.author.company}
42 |
43 |
44 |
45 |
46 |
47 | course icon 51 |
52 |
53 | {courseInfo.frontendMastersLink ? ( 54 |
55 | Watch on Frontend Masters 56 | 57 | ) : null} 58 |
59 |

Table of Contents

60 |
61 |
    62 | {sections.map((section) => ( 63 |
  1. 64 |
    65 |
    66 | 67 |
    68 |
    69 |

    {section.title}

    70 |
      71 | {section.lessons.map((lesson) => ( 72 |
    1. 73 | {lesson.title} 74 |
    2. 75 | ))} 76 |
    77 |
    78 | 79 |
    80 |
  2. 81 | ))} 82 |
83 |
84 |
85 |
86 | 87 | ); 88 | } 89 | 90 | export async function getStaticProps() { 91 | const sections = await getLessons(); 92 | return { 93 | props: { 94 | sections, 95 | }, 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /pages/lessons/[section]/[slug].js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { getLesson, getLessons } from "../../../data/lesson"; 4 | import getCourseConfig from "../../../data/course"; 5 | import Corner from "../../../components/corner"; 6 | import { Context } from "../../../context/headerContext"; 7 | 8 | export default function LessonSlug({ post }) { 9 | const courseInfo = getCourseConfig(); 10 | const [_, setHeader] = useContext(Context); 11 | useEffect(() => { 12 | setHeader({ 13 | section: post.section, 14 | title: post.title, 15 | icon: post.icon, 16 | }); 17 | return () => setHeader({}); 18 | }, []); 19 | 20 | const title = post.title 21 | ? `${post.title} – ${courseInfo.title}` 22 | : courseInfo.title; 23 | const description = post.description 24 | ? post.description 25 | : courseInfo.description; 26 | 27 | return ( 28 | <> 29 | 30 | {title} 31 | 32 | {/* */} 33 | 34 | 35 | 39 | 40 | 41 |
42 |
43 |
47 |
48 | {post.prevSlug ? ( 49 | 50 | ← Previous 51 | 52 | ) : null} 53 | {post.nextSlug ? ( 54 | 55 | Next → 56 | 57 | ) : null} 58 |
59 |
60 | 61 |
62 | 63 | ); 64 | } 65 | 66 | export async function getStaticProps({ params }) { 67 | const post = await getLesson(params.section, params.slug); 68 | return { 69 | props: { 70 | post, 71 | }, 72 | }; 73 | } 74 | 75 | export async function getStaticPaths() { 76 | const sections = await getLessons(); 77 | const lessons = sections.map((section) => section.lessons); 78 | const slugs = lessons.flat().map((lesson) => lesson.fullSlug); 79 | 80 | return { paths: slugs, fallback: false }; 81 | } 82 | -------------------------------------------------------------------------------- /public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/.nojekyll -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/author.jpg -------------------------------------------------------------------------------- /public/images/course-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/course-icon.png -------------------------------------------------------------------------------- /public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/social-share-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v8/f859abce8c9ac6f1dca9ee6932dff0470c39583f/public/images/social-share-cover.jpg -------------------------------------------------------------------------------- /styles/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | padding: 50px 15px; 4 | background-color: var(--primary); 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | color: var(--text-footer); 9 | } 10 | 11 | .socials { 12 | display: flex; 13 | align-items: center; 14 | max-width: 900px; 15 | width: 100%; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .social { 21 | display: inline-block; 22 | list-style: none; 23 | margin-right: 40px; 24 | } 25 | 26 | .social img:hover { 27 | opacity: 0.4; 28 | } 29 | 30 | .social img { 31 | transition: opacity 0.25s; 32 | width: 30px; 33 | } 34 | 35 | .terms { 36 | font-size: 10px; 37 | } 38 | 39 | .terms p { 40 | margin: 3px; 41 | } 42 | 43 | .footer a { 44 | color: inherit; 45 | text-decoration: underline; 46 | } 47 | 48 | .social svg { 49 | transition: opacity 0.25s; 50 | } 51 | 52 | .social svg:hover { 53 | opacity: 0.4; 54 | } 55 | -------------------------------------------------------------------------------- /styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #4dd0e1; 3 | --secondary: #333; 4 | 5 | --highlight: #0097a7; 6 | 7 | --text-header: var(--primary); 8 | --text-main-headers: var(--secondary); 9 | --text-links: var(--highlight); 10 | --text-footer: #333; 11 | 12 | --bg-main: white; 13 | --bg-dots: var(--highlight); 14 | --bg-lesson: white; 15 | 16 | --nav-buttons: var(--primary); 17 | --nav-buttons-text: white; 18 | 19 | --corner-active: var(--primary); 20 | --corner-inactive: #f4f4f4; 21 | --icons: var(--primary); 22 | --footer-icons: var(--secondary); 23 | 24 | --emphasized-bg: #dce8ff; 25 | --emphasized-border: #aab6d2; 26 | } 27 | --------------------------------------------------------------------------------