├── .editorconfig ├── .github └── workflows │ └── next.yaml ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── corner.js ├── footer.js ├── github.js ├── header.js ├── layout.js ├── linkedin.js └── twitter.js ├── context ├── courseInfoContext.js └── headerContext.js ├── course.json ├── 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-parcel.md │ ├── F-browserslist.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-class-components.md │ ├── C-class-properties.md │ ├── D-managing-state-in-class-components.md │ └── meta.json ├── 06-special-case-react-tools │ ├── A-error-boundaries.md │ ├── B-context.md │ ├── C-portals-and-refs.md │ └── meta.json ├── 07-end-of-intro │ ├── A-conclusion.md │ ├── B-ways-to-expand-your-app.md │ └── meta.json ├── 08-intermediate-react-v4 │ └── A-welcome-to-intermediate-react-v4.md ├── 09-hooks-in-depth │ ├── A-usestate.md │ ├── B-useeffect.md │ ├── C-usecontext.md │ ├── D-useref.md │ ├── E-usereducer.md │ ├── F-usememo.md │ ├── G-usecallback.md │ ├── H-uselayouteffect.md │ ├── I-useimperativehandle.md │ ├── J-usedebugvalue.md │ └── meta.json ├── 10-tailwindcss │ ├── A-css-and-react.md │ ├── B-tailwind-basics.md │ ├── C-tailwind-plugins.md │ ├── D-grid-and-breakpoints.md │ ├── E-positioning.md │ └── meta.json ├── 12-code-splitting │ ├── A-code-splitting.md │ └── meta.json ├── 13-server-side-rendering │ ├── A-server-side-rendering.md │ ├── B-streaming-markup.md │ └── meta.json ├── 14-typescript │ ├── A-refactor-modal.md │ ├── B-typescript-and-eslint.md │ ├── C-refactor-theme-context.md │ ├── D-refactor-details.md │ ├── E-refactor-error-boundary.md │ ├── F-refactor-carousel.md │ ├── G-refactor-pet.md │ ├── H-refactor-breed-list.md │ ├── I-refactor-search-params.md │ ├── J-refactor-results.md │ ├── K-refactor-app.md │ └── meta.json ├── 15-redux │ ├── A-redux.md │ ├── B-reducers.md │ ├── C-action-creators.md │ ├── D-providers.md │ ├── E-dispatching-actions.md │ ├── F-redux-dev-tools.md │ └── meta.json ├── 16-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-istanbul.md │ ├── H-visual-studio-code-extension.md │ └── meta.json └── 17-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 ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico └── images │ ├── author.jpg │ ├── brian.jpg │ ├── course-icon.png │ └── social-share-cover.jpg └── styles ├── courses.css ├── footer.css └── variables.css /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.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 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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/courses/complete-react-v7/ 22 | [course]: https://bit.ly/reactv7 23 | [project]: https://github.com/btholt/citr-v7-project/ 24 | 25 | [React icons created by Freepik - 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 | 32 | 39 |
40 |
41 | ); 42 | } 43 | 44 | export default function App({ children }) { 45 | return {children}; 46 | } 47 | -------------------------------------------------------------------------------- /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 v7", 7 | "subtitle": "and Intermediate React v4", 8 | "frontendMastersLink": "https://frontendmasters.com/courses/complete-react-v7/", 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", "parcel"], 16 | "productionBaseUrl": "/complete-intro-to-react-v7" 17 | } 18 | -------------------------------------------------------------------------------- /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 * as matter from "gray-matter"; 4 | import { titleCase } from "title-case"; 5 | import { marked } from "marked"; 6 | import hljs from "highlight.js"; 7 | 8 | marked.setOptions({ 9 | baseUrl: process.env.BASE_URL ? process.env.BASE_URL + "/" : "/", 10 | highlight: function (code, lang) { 11 | const language = hljs.getLanguage(lang) ? lang : "plaintext"; 12 | return hljs.highlight(code, { language }).value; 13 | }, 14 | langPrefix: "hljs language-", 15 | }); 16 | 17 | const DEFAULT_ICON = "info-circle"; 18 | const lessonsPath = path.join(process.env.ROOT, "lessons"); 19 | 20 | function getTitle(slug, override) { 21 | let title = override; 22 | if (!title) { 23 | title = titleCase(slug.split("-").join(" ")); 24 | } 25 | 26 | return title; 27 | } 28 | 29 | async function getMeta(section) { 30 | let meta = {}; 31 | try { 32 | const file = await fs.readFile( 33 | path.join(lessonsPath, section, "meta.json") 34 | ); 35 | meta = JSON.parse(file.toString()); 36 | } catch (e) { 37 | // no meta.json, nothing to do 38 | } 39 | 40 | return meta; 41 | } 42 | 43 | function slugify(inputPath) { 44 | const pathParts = inputPath.split("-"); 45 | const pathOrder = pathParts.shift(); 46 | const pathSlug = pathParts.join("-"); 47 | return { 48 | slug: pathSlug, 49 | order: pathOrder, 50 | title: titleCase(pathParts.join(" ")), 51 | }; 52 | } 53 | 54 | export async function getLessons() { 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}`, 107 | path: filePath, 108 | }); 109 | } 110 | 111 | sections.push({ 112 | icon, 113 | title: sectionTitle, 114 | slug: sectionSlug, 115 | lessons, 116 | order: sectionOrder, 117 | }); 118 | } 119 | 120 | return sections; 121 | } 122 | 123 | export async function getLesson(targetDir, targetFile) { 124 | const dir = await fs.readdir(lessonsPath); 125 | 126 | for (let i = 0; i < dir.length; i++) { 127 | const dirPath = dir[i]; 128 | if (dirPath.endsWith(targetDir)) { 129 | const lessonDir = ( 130 | await fs.readdir(path.join(lessonsPath, dirPath)) 131 | ).filter((str) => str.endsWith(".md")); 132 | 133 | for (let j = 0; j < lessonDir.length; j++) { 134 | const slugPath = lessonDir[j]; 135 | if (slugPath.endsWith(targetFile + ".md")) { 136 | const filePath = path.join(lessonsPath, dirPath, slugPath); 137 | const file = await fs.readFile(filePath); 138 | const { data, content } = matter(file.toString()); 139 | const html = marked(content); 140 | const title = getTitle(targetFile, data.title); 141 | const meta = await getMeta(dirPath); 142 | 143 | const section = getTitle(targetDir, meta.title); 144 | const icon = meta.icon ? meta.icon : DEFAULT_ICON; 145 | 146 | let nextSlug; 147 | let prevSlug; 148 | 149 | // get next 150 | if (lessonDir[j + 1]) { 151 | // has next in section 152 | const { slug: next } = slugify(lessonDir[j + 1]); 153 | nextSlug = `${targetDir}/${next.replace(/\.md$/, "")}`; 154 | } else if (dir[i + 1]) { 155 | // has next in next section 156 | const nextDir = ( 157 | await fs.readdir(path.join(lessonsPath, dir[i + 1])) 158 | ).filter((str) => str.endsWith(".md")); 159 | const nextDirSlug = slugify(dir[i + 1]).slug; 160 | const nextLessonSlug = slugify(nextDir[0]).slug.replace( 161 | /\.md$/, 162 | "" 163 | ); 164 | nextSlug = `${nextDirSlug}/${nextLessonSlug}`; 165 | } else { 166 | // last section 167 | nextSlug = null; 168 | } 169 | 170 | // get prev 171 | if (lessonDir[j - 1]) { 172 | // has prev in section 173 | const { slug: prev } = slugify(lessonDir[j - 1]); 174 | prevSlug = `${targetDir}/${prev.replace(/\.md$/, "")}`; 175 | } else if (dir[i - 1]) { 176 | // has prev in prev section 177 | const prevDir = ( 178 | await fs.readdir(path.join(lessonsPath, dir[i - 1])) 179 | ).filter((str) => str.endsWith(".md")); 180 | const prevDirSlug = slugify(dir[i - 1]).slug; 181 | const prevLessonSlug = slugify( 182 | prevDir[prevDir.length - 1] 183 | ).slug.replace(/\.md$/, ""); 184 | prevSlug = `${prevDirSlug}/${prevLessonSlug}`; 185 | } else { 186 | // first section 187 | prevSlug = null; 188 | } 189 | 190 | const base = process.env.BASE_URL ? process.env.BASE_URL : "/"; 191 | 192 | return { 193 | attributes: data, 194 | html, 195 | slug: targetFile, 196 | title, 197 | section, 198 | icon, 199 | filePath, 200 | nextSlug: nextSlug ? path.join(base, "lessons", nextSlug) : null, 201 | prevSlug: prevSlug ? path.join(base, "lessons", prevSlug) : null, 202 | }; 203 | } 204 | } 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | -------------------------------------------------------------------------------- /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 | > React version 18 is just around the corner but they still haven't settled on the correct APIs yet. As this is meant to be a practical course, I'm going to teach you what's useful today and get you ready to write what's best practices now. When version 18 comes out we'll release a revised version with best practices for version 18. 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 | ReactDOM.render(React.createElement(App), document.getElementById("root")); 58 | ``` 59 | 60 | This is about the simplest React app you can build. 61 | 62 | - 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. 63 | - There are two types of components, function components and class components. This is a function component. We'll see class components shortly. 64 | - A function component _must_ return markup (which is what `React.createElement` generates.) 65 | - These component render functions _have_ to be fast. This function is going to be called a lot. It's a hot code path. 66 | - 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. 67 | - `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.) 68 | - 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.) 69 | - 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.render`. This is how we signal to React where we want it to render our app. Note later we can call `ReactDOM.render` again to change what the root of our React app looks like (I rarely need to do that.) 70 | - Notice we're using `React.createElement` with `App` as a parameter to `ReactDOM.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. 71 | 72 | > ReactDOM.createRoot is a new API as of React v18. The old `ReactDOM.render` that we're using here is still available (but will be deprecated) in v18, you'll just need to update it to opt into future features in v18 (whenever that gets released.) 73 | 74 | [webdev]: https://frontendmasters.com/courses/web-development-v2/ 75 | [style]: https://raw.githubusercontent.com/btholt/citr-v7-project/master/01-no-frills-react/src/style.css 76 | -------------------------------------------------------------------------------- /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 | ReactDOM.render(React.createElement(App), document.getElementById("root")); 28 | ``` 29 | 30 | > 🚨 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 swapped in order in a list and 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. 31 | 32 | Replace your `script` tag in your index.html that has all your code in it with ``. Leave the two React scripts. 33 | 34 | - To make an element have multiple children, just pass it an array of elements. 35 | - 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 into a component like we did here. 36 | - Since we have a new `Pet` component, we can use it multiple times! We just use multiple calls to `React.createElement`. 37 | - 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. 38 | 39 | 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. 40 | 41 | ```javascript 42 | const Pet = (props) => { 43 | return React.createElement("div", {}, [ 44 | React.createElement("h1", {}, props.name), 45 | React.createElement("h2", {}, props.animal), 46 | React.createElement("h2", {}, props.breed), 47 | ]); 48 | }; 49 | 50 | const App = () => { 51 | return React.createElement("div", {}, [ 52 | React.createElement("h1", {}, "Adopt Me!"), 53 | React.createElement(Pet, { 54 | name: "Luna", 55 | animal: "Dog", 56 | breed: "Havanese", 57 | }), 58 | React.createElement(Pet, { 59 | name: "Pepper", 60 | animal: "Bird", 61 | breed: "Cockatiel", 62 | }), 63 | React.createElement(Pet, { name: "Doink", animal: "Cat", breed: "Mix" }), 64 | ]); 65 | }; 66 | 67 | ReactDOM.render(React.createElement(App), document.getElementById("root")); 68 | ``` 69 | 70 | 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! 71 | 72 | > 🏁 [Click here to see the state of the project up until now: 01-no-frills-react][step] 73 | 74 | [step]: https://github.com/btholt/citr-v7-project/tree/master/01-no-frills-react 75 | -------------------------------------------------------------------------------- /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 into 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 script.js`. This will output the formatted version of your file. If you want to actually write the file, run `prettier --write script.js`. Go check script.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.5.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 | ## Alternatives 40 | 41 | There really aren't any for Prettier. The alternative is just not to use a formatter. ESLint's `--fix` flag would be the closest thing. 42 | 43 | [jlongster]: https://twitter.com/jlongster 44 | [prettier]: https://github.com/prettier/prettier 45 | [prettier-playground]: https://prettier.io/playground/ 46 | [prettier-options]: https://prettier.io/docs/en/options.html 47 | [flow]: https://flow.org/ 48 | [prettier-ide]: https://github.com/prettier/prettier#editor-integration 49 | [ts]: https://www.typescriptlang.org/ 50 | [vscode]: https://code.visualstudio.com/?WT.mc_id=reactintro-github-brholt 51 | [vscode-prettier]: https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode&WT.mc_id=reactintro-github-brholt 52 | -------------------------------------------------------------------------------- /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 care 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 this problems. 7 | 8 | First of all, run `npm install -D eslint@8.8.0 eslint-config-prettier@8.3.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 script.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 lint both JS and JSX. 46 | 47 | ESLint is a cinch to get working with [Visual Studio Code][vscode]. Just down [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 | .parcel-cache/ 12 | dist/ 13 | .env 14 | .DS_Store 15 | coverage/ 16 | .vscode/ 17 | ``` 18 | 19 | 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 courses on Frontend Masters][nina] 20 | 21 | [nina]: https://frontendmasters.com/courses/git-in-depth/ 22 | -------------------------------------------------------------------------------- /lessons/03-js-tools/E-parcel.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 Parcel which makes the whole process a breeze." 3 | --- 4 | 5 | [Parcel][parcel] is a great bundler for JavaScript projects. The first three revisions of this workshop all teach Webpack and end up spending a decent amount of time covering how to set it up. Webpack is a fantastic piece of technology and you should definitely consider using it for your large applications; it's been around a long time and has a lot of support. 6 | 7 | That being said, Parcel is an amazing tool with zero-config. It works with everything we want to do out of the box. Since this is a class on React and not build processes, this allows us to focus more on React. Let's go see what it can do for us. 8 | 9 | Parcel is going to accept an entry point, crawl through all of its dependencies, and output a single, complete file with all of our code in it. This means we can have large applications with many files and many dependencies. It would be an unmanageable mess. Already our React app has two components in one file: App and Pet. It'd be better if these were in separate files so it'd be easier to keep track of what was where. This is where Parcel can help us. 10 | 11 | Install Parcel by doing `npm install -D parcel@2.2.1` 12 | 13 | > Parcel has renamed the npm package from `parcel-bundler` to just `parcel`. 14 | 15 | Now inside of your `package.json` put: 16 | 17 | ```json 18 | "scripts" { 19 | "dev": "parcel src/index.html" 20 | } 21 | ``` 22 | 23 | Now open http://localhost:1234. You should see your site come up. The difference here is now it's being run through Parcel which means we can leverage all the features that Parcel allows us which we will explore shortly. 24 | 25 | So how does it work? We gave the entry point, which is index.html. It then reads that index.html file and finds its dependencies, which are the two React files and the one App.js file that we linked to. It's smart enough to detect that those two React files are remote so it doesn't do anything with those, but it sees that App.js is local and so it reads it and compiles its dependencies. Right now it has no dependencies so let's fix that. 26 | 27 | First let's fix the React and ReactDOM dependencies. Right now these are coming from unpkg.com. Unpkg isn't meant to serve production traffic, nor do we want the burden of loading _all_ of our future dependencies this way. Believe me, it would get messy quickly and we'd have to make a million requests to get all of them by the end (we'll install more later as we go.) Instead, it'd be better if we could pull our dependencies down from npm and include it in our bundle. Let's do that now. 28 | 29 | Run `npm install react@17.0.2 react-dom@17.0.2`. This will pull React and ReactDOM down from npm and put it in your node_modules directory. Now instead of loading them from unpkg, we can tell Parcel to include them in your main bundle. Let's do that now. 30 | 31 | > As of writing, only the release candidate of React v18 is out. You may check to see if the actual release out (nothing should have changed between the rc and the actual release.) This course will focus on v17. 32 | 33 | Delete the two unpkg script tags in index.html. Replace the App.js import with ``. This is how Parcel knows that we're using the latest JS. 34 | 35 | > Having to add `type="module"` is new to Parcel 2. 36 | 37 | Refresh the page and it still works! Now our React and ReactDOM is loading directly from our bundle instead of separate JavaScript files! Let's take this a step further. Create a new file called Pet.js and put this in there: 38 | 39 | ```javascript 40 | import React from "react"; 41 | 42 | export default function Pet({ name, animal, breed }) { 43 | return React.createElement("div", {}, [ 44 | React.createElement("h1", {}, name), 45 | React.createElement("h2", {}, animal), 46 | React.createElement("h2", {}, breed), 47 | ]); 48 | } 49 | ``` 50 | 51 | Go to App.js 52 | 53 | ```javascript 54 | // at the top, under React imports 55 | import React from "react"; 56 | import ReactDOM from "react-dom"; 57 | import Pet from "./Pet"; 58 | 59 | // remove Pet component 60 | ``` 61 | 62 | Load the page again. Still works! Now we can separate components into separate files. Parcel does more than just this but we'll get to that in future sections. 63 | 64 | > Please close the `file:///` browser tabs you have open and only use the `localhost:1234` ones. Now that we're using Parcel the former won't work anymore! 65 | 66 | ## Alternatives 67 | 68 | - [Webpack][webpack] 69 | - [Browserify][browserify] 70 | - [esbuild][esbuild] 71 | - [Rollup][rollup] 72 | 73 | [browserify]: http://browserify.org/ 74 | [webpack]: https://webpack.js.org/ 75 | [parcel]: https://parceljs.org/ 76 | [rollup]: https://www.rollupjs.org/ 77 | [esbuild]: https://esbuild.github.io/ 78 | -------------------------------------------------------------------------------- /lessons/03-js-tools/F-browserslist.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Babel transforms your JS code from futuristic code to code that is understandable by older browsers. Via a package called browserslist (which Parcel installed for you) you can tell Babel what browsers to target." 3 | --- 4 | 5 | Babel transforms your JS code from futuristic code to code that is understandable by older browsers. Via a package called browserslist (which Parcel installed for you) you can Babel what browsers to target. 6 | 7 | Head to [browserslist.dev][dev]. 8 | 9 | Here you can see what sorts of queries you can provide to browsers and which browsers those will provide. By default Parcel uses `> .25%` which targets all browsers that have more than .25% usage. When I ran as of writing this, this would cover 92% of all Internet users. That's a pretty okay range to target depending on who your audience is. If you're targeting old government computers this won't work but if you're Airbnb and targeting mostly modern browsers you're probably okay. 10 | 11 | Since we're targeting ourselves and this is just a dev project, let's just target ourselves. Put `last 2 Chrome versions`, `last 2 Firefox versions`, `last 2 Edge versions`, `last 2 Safari versions`, or whatever you want. For the course notes I'll put `last 2 Chrome versions` since I've observed many of my students use Chrome. 12 | 13 | In your package.json, add a new top level field called `browserslist` (notice the `s`, browser**s**list): 14 | 15 | ```json 16 | { 17 | … 18 | "browserslist": [ 19 | "last 2 Chrome versions" 20 | ] 21 | } 22 | ``` 23 | 24 | > 🏁 [Click here to see the state of the project up until now: 02-js-tools][step] 25 | 26 | [step]: https://github.com/btholt/citr-v7-project/tree/master/02-js-tools 27 | [dev]: https://browserslist.dev 28 | -------------------------------------------------------------------------------- /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. Let's convert Pet.js to using JSX. It will look like this: 9 | 10 | ```javascript 11 | // delete the import 12 | 13 | const Pet = (props) => { 14 | return ( 15 |
16 |

{props.name}

17 |

{props.animal}

18 |

{props.breed}

19 |
20 | ); 21 | }; 22 | 23 | export default Pet; 24 | ``` 25 | 26 | > 🚨 ESLint may be currently failing. We'll fix it at the end. 27 | 28 | 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. 29 | 30 | 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. 31 | 32 | 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. 33 | 34 | > 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! 35 | 36 | Notice you still have to import React despite React not being explicitly used. Remember that JSX is compiled to `React.createElement` calls. Anywhere you use JSX, you need to import React. 37 | 38 | So now JSX is demystified a bit, let's go convert App.js. 39 | 40 | ```javascript 41 | import { render } from "react-dom"; 42 | import Pet from "./Pet"; 43 | 44 | const App = () => { 45 | return ( 46 |
47 |

Adopt Me!

48 | 49 | 50 | 51 |
52 | ); 53 | }; 54 | 55 | render(, document.getElementById("root")); 56 | ``` 57 | 58 | > 🚨 ESLint is currently failing. We'll fix it at the end. 59 | 60 | 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. 61 | 62 | We now pass props down as we add attributes to an HTML tag. Pretty cool. 63 | 64 | ## ESLint + React 65 | 66 | 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. 67 | 68 | Run this: `npm install -D eslint-plugin-import@2.25.4 eslint-plugin-jsx-a11y@6.5.1 eslint-plugin-react@7.28.0` 69 | 70 | Update your .eslintrc.json to: 71 | 72 | ```javascript 73 | { 74 | "extends": [ 75 | "eslint:recommended", 76 | "plugin:import/errors", 77 | "plugin:react/recommended", 78 | "plugin:jsx-a11y/recommended", 79 | "prettier" 80 | ], 81 | "rules": { 82 | "react/prop-types": 0, 83 | "react/react-in-jsx-scope": 0 84 | }, 85 | "plugins": ["react", "import", "jsx-a11y"], 86 | "parserOptions": { 87 | "ecmaVersion": 2022, 88 | "sourceType": "module", 89 | "ecmaFeatures": { 90 | "jsx": true 91 | } 92 | }, 93 | "env": { 94 | "es6": true, 95 | "browser": true, 96 | "node": true 97 | }, 98 | "settings": { 99 | "react": { 100 | "version": "detect" 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | > 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. 107 | 108 | 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. 109 | 110 | 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. 111 | 112 | - The import plugin helps ESLint catch common bugs around imports, exports, and modules in general. 113 | - jsx-a11y catches many bugs around accessibility that can accidentally arise using React, like not having an `alt` attribute on an `img` tag. 114 | - react is mostly common React bugs like not calling one of your props children. 115 | - `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. 116 | - `"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. 117 | - Prop types allow you to add runtime type props to a component. In general if you're interested in doing that just use TypeScript. 118 | 119 | Now your project should pass lint. 120 | 121 | > 🏁 [Click here to see the state of the project up until now: 03-jsx][step] 122 | 123 | [airbnb]: https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb 124 | [standard]: https://standardjs.com/ 125 | [step]: https://github.com/btholt/citr-v7-project/tree/master/03-jsx 126 | [prettier-react]: https://github.com/prettier/eslint-config-prettier#installation 127 | -------------------------------------------------------------------------------- /lessons/04-core-react-concepts/C-effects.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "useEffect is a criticl 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 add that initial list of pets. 6 | 7 | Add this to SearchParams.js 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 | > 🚨 If you're seeing an error about regeneratorRuntime, please go back to the Browserslist lesson and make sure you followed that. If you have and it's still broke, please delete your node_modules, .parcel-cache, and dist directories, run `npm install` again, and try running `npm run dev` again. 40 | 41 | - 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. 42 | - 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. 43 | - 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. 44 | - 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. 45 | - 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 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. 46 | - At the end, we take the pets we got back from the API and create Pet components out of each of them. 47 | 48 | > 🏁 [Click here to see the state of the project up until now: 05-useeffect][step] 49 | 50 | [step]: https://github.com/btholt/citr-v7-project/tree/master/05-useeffect 51 | -------------------------------------------------------------------------------- /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 it 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 | ```javascript 12 | import { useState, useEffect } from "react"; 13 | 14 | const localCache = {}; 15 | 16 | export default function useBreedList(animal) { 17 | const [breedList, setBreedList] = useState([]); 18 | const [status, setStatus] = useState("unloaded"); 19 | 20 | useEffect(() => { 21 | if (!animal) { 22 | setBreedList([]); 23 | } else if (localCache[animal]) { 24 | setBreedList(localCache[animal]); 25 | } else { 26 | requestBreedList(); 27 | } 28 | 29 | async function requestBreedList() { 30 | setBreedList([]); 31 | setStatus("loading"); 32 | const res = await fetch( 33 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 34 | ); 35 | const json = await res.json(); 36 | localCache[animal] = json.breeds || []; 37 | setBreedList(localCache[animal]); 38 | setStatus("loaded"); 39 | } 40 | }, [animal]); 41 | 42 | return [breedList, status]; 43 | } 44 | ``` 45 | 46 | - 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. 47 | - 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. 48 | - 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. 49 | 50 | Head over to SearchParam.js and put this in there. 51 | 52 | ```javascript 53 | import useBreedList from "./useBreedList"; 54 | 55 | // replace `const breeds = [];` 56 | const [breeds] = useBreedList(animal); 57 | ``` 58 | 59 | 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.) 60 | 61 | > 🏁 [Click here to see the state of the project up until now: 06-custom-hooks][step] 62 | 63 | [step]: https://github.com/btholt/citr-v7-project/tree/master/06-custom-hooks 64 | -------------------------------------------------------------------------------- /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 tooltip, 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 Pet components. Make a new file called Results.js. 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.js and put this: 44 | 45 | ```javascript 46 | // at top, replace import from Pet.js 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.js 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-v7-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 Parcel.js, it will compile your development server with an environment variable of `NODE_ENV=development` and then when you run `parcel 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 | Go to App.js and wrap `` in the render call in ``. 20 | 21 | ```javascript 22 | // import at top 23 | import { StrictMode } from "react"; 24 | 25 | // replace App 26 | const App = () => { 27 | return ( 28 | 29 |
30 |

Adopt Me!

31 | 32 |
33 |
34 | ); 35 | }; 36 | ``` 37 | 38 | ## Dev Tools 39 | 40 | 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. 41 | 42 | [dev-tools]: https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-devtools-profiler 43 | -------------------------------------------------------------------------------- /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], stay tuned for a Frontend Masters Remix course!) 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 find out more about each animal. 10 | 11 | Let's quickly make a second page so we can switch between the two. Make file called Details.js. 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.2.1`. 22 | 23 | > If you're getting a dependency error and it won't let you install it, add `--force` to override npm and install it anyway. 24 | 25 | Now we have two pages and the router available. Let's go make it ready to switch between the two. In `App.js`: 26 | 27 | ```javascript 28 | // at top 29 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 30 | import Details from "./Details"; 31 | 32 | // replace and

Adopt Me!

33 | 34 |

Adopt Me!

35 | 36 | } /> 37 | } /> 38 | 39 |
; 40 | ``` 41 | 42 | > If you're upset about the element prop vs children, [read their rationale here][element] 43 | 44 | Now we have the router working (but still have an issue)! Try navigating to http://localhost:1234/ and then to http://localhost:1234/details/1. Both should work … sort of! 45 | 46 | - React Router has a ton of features that we're not going to explain here. The docs do a great job. 47 | - The `:id` part is a variable. In `http://localhost:1234/details/1`, `1` would be the variable. 48 | - 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. 49 | - 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. 50 | - 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. 51 | 52 | So now let's make the two pages link to each other. Go to Pet.js. 53 | 54 | ```javascript 55 | // at top 56 | import { Link } from "react-router-dom"; 57 | 58 | // change wrapping 59 | 60 | […] 61 | ; 62 | ``` 63 | 64 | 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. 65 | 66 | 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: 67 | 68 | ```javascript 69 | import { useParams } from "react-router-dom"; 70 | 71 | const Details = () => { 72 | const { id } = useParams(); 73 | return

{id}

; 74 | }; 75 | 76 | export default Details; 77 | ``` 78 | 79 | The `useParams` hook is how you get params from React Router. It used to be through the props but now they prefer this API. 80 | 81 | Let's make the Adopt Me! header clickable too. 82 | 83 | ```javascript 84 | // import Link too 85 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 86 | 87 | // replace h1 88 |
89 | Adopt Me! 90 |
; 91 | ``` 92 | 93 | > If you're getting a useHref error, make sure your `header` is _inside_ `` 94 | 95 | Now if you click the header, it'll take you back to the Results page. Cool. Now let's round out the Details page. 96 | 97 | > 🏁 [Click here to see the state of the project up until now: 08-react-router][step] 98 | 99 | [rr]: https://reacttraining.com/react-router/ 100 | [reach]: https://reach.tech/router/ 101 | [rf]: https://twitter.com/ryanflorence 102 | [step]: https://github.com/btholt/citr-v7-project/tree/master/08-react-router 103 | [remix]: https://remix.run 104 | [element]: https://reactrouter.com/docs/en/v6/upgrading/v5#advantages-of-route-element 105 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/B-class-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "While many components are written with hooks, the older API of class-based components are still around and still useful. Brian shows you when and how to use the class components API." 3 | --- 4 | 5 | This class has been showing you the latest APIs for React: hooks. Going forward, these sorts of components will be the default way of writing React. However, the class API still has its uses and isn't going anywhere anytime soon. In this section we're going to go through and learn the basics of it since there's still a lot class code out in the wild and the new API can't do _everything_ the old one can, so it's still useful in some cases. 6 | 7 | Let's go make Details.js as a class. 8 | 9 | ```javascript 10 | // replace Details.js 11 | import { Component } from "react"; 12 | import { useParams } from "react-router-dom"; 13 | 14 | class Details extends Component { 15 | constructor() { 16 | super(); 17 | this.state = { loading: true }; 18 | } 19 | 20 | async componentDidMount() { 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/pets?id=${this.props.params.id}` 23 | ); 24 | const json = await res.json(); 25 | this.setState(Object.assign({ loading: false }, json.pets[0])); 26 | } 27 | 28 | render() { 29 | if (this.state.loading) { 30 | return

loading …

; 31 | } 32 | 33 | const { animal, breed, city, state, description, name } = this.state; 34 | 35 | return ( 36 |
37 |
38 |

{name}

39 |

{`${animal} — ${breed} — ${city}, ${state}`}

40 | 41 |

{description}

42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | const WrappedDetails = () => { 49 | const params = useParams(); 50 | return
; 51 | }; 52 | 53 | export default WrappedDetails; 54 | ``` 55 | 56 | - 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`. 57 | - Not every component needs to have a constructor. Many don't. I'll show you momentarily how you nearly never need to have one. In this case we need it to instantiate the state object (which we'll use instead of `useState`.) If you have a constructor, you _have_ to do the `super(props)` to make sure that the props are passed up to React so React can keep track of them. 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 | - 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. 60 | - `this.state` is the mutable state of the component (like useState). You'll use `this.setState` to mutate it (don't modify it directly.) 61 | - `this.props` comes from the parent component, similar to parameter given to the render functions that we pull props out of. 62 | - React Router's API _only_ exposes hooks. If you have a class component that is a route, this is how you can use it, make a wrapper component that uses the hook you need, and then pass that into the component. You'll find yourself frequently making these little wrapper components for things like this. 63 | 64 | > `withRouter` higher order component was deprecated by the React Router team. Their API is hooks only now. [See the reason here][gh-issue]. 65 | 66 | ## Other lifecycle methods 67 | 68 | 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`. 69 | 70 | There are lots more you can check out in [the React docs here][docs]. 71 | 72 | [docs]: https://reactjs.org/docs/react-component.html 73 | [gh-issue]: https://github.com/remix-run/react-router/issues/7256#issuecomment-612642537 74 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/C-class-properties.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Using constructors to set initial state for class components is verbose and can be done better. Brian teaches you to use the new feature in JavaScript, class properties, to make your code easy to read." 3 | --- 4 | 5 | The constructor is annoying. We can use something called class properties to make it a lot nicer and easier to read. Class properties are a new part of JavaScript so we need Parcel transform the code when Parcel transpiles our code. Luckily our config will do that by itself so no further changes are needed (previously we did need to.) 6 | 7 | Parcel will merge this config with what it has already, so we just need to pull in the one Babel plugin we need. 8 | 9 | ```bash 10 | npm i -D @babel/plugin-proposal-class-properties@7.16.7 11 | ``` 12 | 13 | Now make a file called `.babelrc` with the following: 14 | 15 | ```json 16 | { 17 | "plugins": ["@babel/plugin-proposal-class-properties"] 18 | } 19 | ``` 20 | 21 | Babel's core concept is a plugin. Every one sort of a transformation it can perform is encapsulated into a plugin. Here we're including one explicitly: class properties. 22 | 23 | Now with this, we can modify Details to be as so: 24 | 25 | ```javascript 26 | // replace constructor 27 | state = { loading: true }; 28 | ``` 29 | 30 | Loads easier to read, right? 31 | -------------------------------------------------------------------------------- /lessons/05-react-capabilities/D-managing-state-in-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 | Okay, so on this page, notice first we have a loading indicator (this one isn't nice looking but you could put some effort into it if you wanted.) This is a good idea while you're waiting for data to load. 6 | 7 | Let's make a nice photo carousel of the pictures for the animal now. Make a new file called Carousel.js: 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 | Add the Carousel component to the Details page. 47 | 48 | ```javascript 49 | // import at top 50 | import Carousel from "./Carousel"; 51 | 52 | // at top of Details function 53 | const { animal, breed, city, state, description, name, images } = this.state; 54 | 55 | // first component inside div.details 56 | ; 57 | ``` 58 | 59 | - defaultProps does exactly what it sounds like: it allows you to set props that a component has by default if they're not furnished by parent. That way we don't have to do detection logic and can just assume they're always there. 60 | 61 | Let's make it so we can react to someone changing the photo on the carousel. 62 | 63 | ```javascript 64 | // add event listener 65 | handleIndexClick = event => { 66 | this.setState({ 67 | active: +event.target.dataset.index 68 | }); 69 | }; 70 | 71 | // above img 72 | // eslint-disable-next-line 73 | 74 | // add to img 75 | onClick={this.handleIndexClick} 76 | data-index={index} 77 | ``` 78 | 79 | - This is how you handle events in React class components. If it was keyboard handler, you'd do an onChange or onKeyUp, etc. handler. 80 | - 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. 81 | - The data attribute comes back as a string. We want it to be a number, hence the `+`. 82 | - We're doing bad accessibility stuff. But this makes it a lot simpler for learning for now. But don't do this in production. 83 | 84 | > 🏁 [Click here to see the state of the project up until now: 09-managing-state-in-class-components][step] 85 | 86 | [step]: https://github.com/btholt/citr-v7-project/tree/master/09-managing-state-in-class-components 87 | [babel]: https://babeljs.io/ 88 | -------------------------------------------------------------------------------- /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.js 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 or wait five seconds. 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.js 46 | 47 | ```javascript 48 | // add import 49 | import ErrorBoundary from "./ErrorBoundary"; 50 | 51 | // replace WrappedDetails 52 | const WrappedDetails = () => { 53 | const params = useParams(); 54 | return ( 55 | 56 |
57 | 58 | ); 59 | }; 60 | ``` 61 | 62 | - 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. 63 | - 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. 64 | 65 | Let's make it redirect automatically after five seconds. We could do a set timeout in the `componentDidCatch` but let's do it with `componentDidUpdate` to show you how that works. 66 | 67 | ```javascript 68 | // top 69 | import { Link, Navigate } from "react-router-dom"; 70 | 71 | // add redirect 72 | state = { hasError: false, redirect: false }; 73 | 74 | // under componentDidCatch 75 | componentDidUpdate() { 76 | if (this.state.hasError) { 77 | setTimeout(() => this.setState({ redirect: true }), 5000); 78 | } 79 | } 80 | 81 | // first thing inside render 82 | if (this.state.redirect) { 83 | return ; 84 | } } else if (this.state.hasError) { 85 | … 86 | } 87 | ``` 88 | 89 | - `componentDidUpdate` is how you react to state and prop changes with class components. In this case we're reacting to the state changing. You're also passed the previous state and props in the paremeters (which we didn't need) in case you want to detect what changed. 90 | - Rendering Navigate components is how you do redirects with React Router. You can also do it progamatically but I find this approach elegant. 91 | 92 | > 🏁 [Click here to see the state of the project up until now: 10-error-boundaries][step] 93 | 94 | [step]: https://github.com/btholt/citr-v7-project/tree/master/10-error-boundaries 95 | [sentry]: https://sentry.io/ 96 | [trackjs]: https://trackjs.com/ 97 | [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself 98 | -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/B-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 | Historically I have not taught context when teaching React. This was for a couple reasons. First of all, the API they were using was still unofficial, however they standardized it in version 16. Secondly, normally you don't need context; React's state is enough. Thirdly, the old API was bad, in my opinion. The new one is pretty good. 6 | 7 | So here we go. 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. 8 | 9 | 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. 10 | 11 | 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. 12 | 13 | Imagine if we wanted to let the user choose a simple theme for the site. And we want to make that theme stick as the user navigates across different pages. This means the state has to live outside of the route where it's selected. We could use Redux for this, we could use React itself, or we're going to use context, to teach you what that looks like. 14 | 15 | Make a new file called ThemeContext.js: 16 | 17 | ```javascript 18 | import { createContext } from "react"; 19 | 20 | const ThemeContext = createContext(["green", () => {}]); 21 | 22 | export default ThemeContext; 23 | ``` 24 | 25 | `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. 26 | 27 | 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. 28 | 29 | 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. Here we're giving it the shape of a `useState` call because we'll using `useState` with it. You do not have to use context with hooks; [see v4][v4] if you want to see how to do it without hooks. That example also has a more complicated data shape. This example is a lot more simple. If you wanted a more complicated data shape, you'd replace `"green"` with an object full of other properties. 30 | 31 | Now we're going to make all the buttons' background color in the app be governed by the theme. First let's go to App.js: 32 | 33 | ```javascript 34 | // import useState and ThemeContext 35 | import { StrictMode, useState } from "react"; 36 | import ThemeContext from "./ThemeContext"; 37 | 38 | // top of App function body 39 | const theme = useState("darkblue"); 40 | 41 | // wrap the rest of the app 42 | […]; 43 | ``` 44 | 45 | - We're going to use the `useState` hook because theme 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 theme 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 `SearchParams.js`: 51 | 52 | ```javascript 53 | // import at top 54 | import { useState, useEffect, useContext } from "react"; 55 | import ThemeContext from "./ThemeContext"; 56 | 57 | // top of SearchParams function body 58 | const [theme] = useContext(ThemeContext); 59 | 60 | // replace button 61 | ; 62 | ``` 63 | 64 | - Now your button should be a beautiful shade of `darkblue`. 65 | - `useContext` is how you get the context data out of a given context (you can lots of various types of context in a given app.) 66 | - Right now it's just reading from it and a pretty silly use of context. But let's go make Details.js use it as well. 67 | 68 | Let's go do this in Details.js 69 | 70 | ```javascript 71 | // import 72 | import ThemeContext from "./ThemeContext"; 73 | 74 | // replace button 75 | 76 | {([theme]) => ( 77 | 78 | )} 79 | ; 80 | ``` 81 | 82 | - This is how you use context inside of a class component. 83 | - Remember you cannot use hooks inside class components at all. This is why we're using the `Consumer` from `ThemeContext`. Functionally this works the same way though. 84 | 85 | Lastly let's go make the theme changeable. Head back to SearchParams.js. 86 | 87 | ```javascript 88 | // also grab setTheme 89 | const [theme, setTheme] = useContext(ThemeContext); 90 | 91 | // below the breed dropdown 92 | ; 105 | ``` 106 | 107 | - This looks relatively similar to hooks, right? It should because it works the same! 108 | - Now try changing the theme and navigating to a pet page. Notice the theme is consistent! The theme is kept between pages because it's kept at the App level and App is never unmounted so its state persists between route changes. 109 | - You can have multiple layers of context. If I wrapped SearchParams in its own `Provider` (in addition to the one that already exists), the SearchParams context would read from that because it's the closest one whereas Details would read from the top level one because it's the only one. 110 | 111 | That's it for context! Something like theming would be perfect for context. It's for app-level data. Everything else should be boring-ol' state. 112 | 113 | > 🏁 [Click here to see the state of the project up until now: 11-context][step] 114 | 115 | [step]: https://github.com/btholt/citr-v7-project/tree/master/11-context 116 | [v4]: https://btholt.github.io/complete-intro-to-react-v4/context 117 | -------------------------------------------------------------------------------- /lessons/06-special-case-react-tools/C-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.js: 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. If this is confusing to you, we dive deeper into it in [the useRef section of Intermediate React][useref]. 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.js and add: 46 | 47 | ```javascript 48 | // at the top 49 | import Modal from "./Modal"; 50 | 51 | // add showModal 52 | state = { loading: true, showModal: false }; 53 | 54 | // above render 55 | toggleModal = () => this.setState({ showModal: !this.state.showModal }); 56 | 57 | // add showModal 58 | const { animal, breed, city, state, description, name, images, showModal } = 59 | this.state; 60 | 61 | // add onClick to ; 65 | 66 | // below description 67 | { 68 | showModal ? ( 69 | 70 |
77 | 78 | ) : null; 79 | } 80 | ``` 81 | 82 | - We're using an `a` for the adopt functionality to redirect offsite but you could easily make this a function that calls an API to commit the adopt action for your site. 83 | - Notice that despite we're rendering a whole different part of the DOM we're still referencing the state in Details.js. 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. 84 | 85 | 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.) 86 | 87 | > 🏁 [Click here to see the state of the project up until now: 12-portals-and-refs][step] 88 | 89 | [portal]: https://reactjs.org/docs/portals.html 90 | [step]: https://github.com/btholt/citr-v7-project/tree/master/12-portals-and-refs 91 | [useref]: https://btholt.github.io/complete-intro-to-react-v7/lessons/hooks-in-depth/useref 92 | -------------------------------------------------------------------------------- /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 | ## Use the Theme Selector in More Places 107 | 108 | We could do better than just using the theme selector just on buttons. 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 Parcel but consider trying one of the newer build systems like [Vite], [Snowpack], [ESBuild], or any of the others. You could also use one of the popular mainstays like [Webpack][webpack] or [Rollup][rollup]. 117 | 118 | ## Try React 18! 119 | 120 | I really wanted to teach React 18 for this course but it's just not out yet and it could change. However you could give it a shot! [See here to get started][r18] 121 | 122 | ## Let me know 123 | 124 | 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. 125 | 126 | [pf]: https://www.petfinder.com/developers/ 127 | [pf-sdk]: https://github.com/petfinder-com/petfinder-js-sdk 128 | [swa]: https://azure.microsoft.com/en-us/services/app-service/static/ 129 | [gcp]: https://firebase.google.com/ 130 | [aws]: https://aws.amazon.com/amplify/ 131 | [netlify]: https://www.netlify.com/ 132 | [vercel]: https://vercel.com/ 133 | [vite]: https://vitejs.dev/ 134 | [snowpack]: https://www.snowpack.dev/ 135 | [esbuild]: https://esbuild.github.io/ 136 | [webpack]: https://webpack.js.org/ 137 | [rollup]: https://www.rollupjs.org/guide/en/ 138 | [r18]: https://github.com/reactwg/react-18/discussions/112 139 | -------------------------------------------------------------------------------- /lessons/07-end-of-intro/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "stopwatch" 3 | } -------------------------------------------------------------------------------- /lessons/08-intermediate-react-v4/A-welcome-to-intermediate-react-v4.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Welcome to Intermediate React v4" 3 | description: "Brian introduces you to Intermediate React v4 and explains how the course is structured." 4 | --- 5 | 6 | Welcome to Intermediate React v4 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 v7. 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-v7-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 [this folder][project]. This folder is the completed project from the Complete Intro to React v7. After each module, we'll restart with a fresh copy of this repo (i.e. the modules **do not** build on each other.) 15 | 16 | Alright! Let's have some fun with Intermdiate React v4! 17 | 18 | [intro]: https://btholt.github.io/complete-intro-to-react-v7/lessons/welcome/intro 19 | [citr]: https://github.com/btholt/citr-v7-project/ 20 | [project]: https://github.com/btholt/citr-v7-project/tree/master/12-portals-and-refs 21 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/A-usestate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useState" 3 | description: "" 4 | --- 5 | 6 | [Open this CodeSandbox.][codesandbox] All of the examples are in there. We will not be using the code in project for this section, just this CodeSandbox. 7 | 8 | In the preceding course, we went over `useState`, `useEffect`, `useContext`, and `useRef`. These are the most common hooks and likely 99% of what you're going to use. However it's good to know what other tools are in your toolbox for the 1% of problems. We'll go through, example-by-example, and work out what all these hooks can do for you. (we'll review the ones we've talked about already too.) 9 | 10 | [Component][state]. 11 | 12 | `useState` allows us to make our components stateful. Whereas this previously required using a class component, hooks give us the ability to write it using just functions. It allows us to have more flexible components. In our example component, everytime you click on the h1 (bad a11y, by the way) it'll change colors. It's doing this by keeping that bit of state in a hook which is being fed in anew every render so it always has the latest state. 13 | 14 | [codesandbox]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main/ 15 | [state]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/State.js 16 | [closures]: https://frontendmasters.com/courses/javascript-foundations/closure-introduction/ 17 | [fibonacci]: https://en.wikipedia.org/wiki/Fibonacci_number 18 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/B-useeffect.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useEffect" 3 | description: "" 4 | --- 5 | 6 | [Component][effect] 7 | 8 | Effects are how you recreate `componentDidMount`, `componentDidUpdate`, and `componentDidUnmount` from React. Inside `useEffect`, you can do any sort of sidd-effect type action that you would have previously done in one of React's lifecycle method. You can do things like fire AJAX requests, integrate with third party libraries (like a jQuery plugin), fire off some telemetry, or anything else that need to happen on the side for your component. 9 | 10 | In our case, we want our component to continually update to show the time so we use setTimeout inside our effect. After the timeout calls the callback, it updates the state. After that render happens, it schedules another effect to happen, hence why it continues to update. You could provide a second parameter of `[]` to `useEffect` (after the function) which would make it only update once. This second array is a list of dependencies: only re-run this effect if one of these parameters changed. In our case, we want to run after every render so we don't give it this second parameter. 11 | 12 | [effect]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Effect.js 13 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/C-usecontext.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useContext" 3 | description: "" 4 | --- 5 | 6 | [Component][context] 7 | 8 | An early problem with the React problem is called "data tunneling" or "prop drilling". This is when you have a top level component (in our case the parent component) and a child component way down in the hierarchy that need the same data (like the user object.) We could pass that data down, parent-to-child, for each of the intermediary components but that sucks because now each of `LevelTwo`, `LevelThree`, and `LevelFour` all have to know about the user object even when they themselves don't need it, just their children. This is prop drilling: passing down this data in unnecessary intermediaries. 9 | 10 | Enter context. Context allows you to create a wormhole where stuff goes in and a wormhole in a child component where that same data comes out and the stuff in the middle doesn't know it's there. Now that data is available anywhere inside of the `UserContext.Provider`. `useContext` just pulls that data out when given a Context object as a parameter. You don't have to use `useState` and `useContext` together (the data can be any shape, not just `useState`-shaped) but I find it convenient when child components need to be able to update the context as well. 11 | 12 | In general, context adds a decent amount of complexity to an app. A bit of prop drilling is fine. Only put things in context that are truly application-wide state like user information or auth keys and then use local state for the rest. 13 | 14 | Often you'll use context instead of Redux or another state store. You could get fancy and use `useReducer` and `useContext` together to get a pretty great approximation of Redux-like features. 15 | 16 | [context]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Context.js 17 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/D-useref.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useRef" 3 | description: "" 4 | --- 5 | 6 | [Component][ref] 7 | 8 | Refs are useful for several things, we'll explore two of the main reasons in these examples. I want to show you the first use case: how to emulate instance variables from React. 9 | 10 | In order to understand why refs are useful, you need to understand [how closures work][closures]. In our component, when a user clicks, it sets a timeout to log both the state and the ref's number after a second. One thing to keep in mind that **the state and the ref's number are always the same**. They are never out of lockstep since they're updated at the same time. _However_, since we delay the logging for a second, when it alerts the new values, it will capture what the state was when we first called the timeout (since it's held on to by the closure) but it will always log the current value since that ref is on an object that React consistently gives the same object back to you. Because it's the same object and the number is a property on the object, it will always be up to date and not subject to the closure's scope. 11 | 12 | Why is this useful? It can be useful for things like holding on to `setInterval` and `setTimeout` IDs so they can be cleared later. Or any bit of statefulness that _could_ change but you don't want it to cause a re-render when it does. 13 | 14 | It's also useful for referencing DOM nodes directly and we'll see that a bit later in this section. 15 | 16 | [ref]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Ref.js 17 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/E-usereducer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useReducer" 3 | description: "" 4 | --- 5 | 6 | [Component][reducer] 7 | 8 | I'm going to assume you're familiar with Redux. If not, there's a brief section on it [here](redux-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. 9 | 10 | [reducer]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Reducer.js 11 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/F-usememo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useMemo" 3 | description: "useMemo memoizes expensive function calls so they only are re-evaluated when needed." 4 | --- 5 | 6 | [Component][memo] 7 | 8 | `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. 9 | 10 | `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 the previous, memoized answer if it hasn't changed. 11 | 12 | If we didn't have the `useMemo` call, everytime I clicked on the title to cause the color to change from red to green or vice versa it'd unnecessarily recalculate the answer of `fibonacci` but because we did use `useMemo` it will only calculate it when `num` has changed. 13 | 14 | Feel try to remove `useMemo`, get `num` to 40 or so, and then click the h1. It'll be slow. 15 | 16 | [memo]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Memo.js 17 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/G-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 | [Component][callback] 7 | 8 | `useCallback` is quite similar and indeed it's implemented with the same mechanisms as `useMemo`. Our goal is that `ExpensiveComputationComponent` 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. 9 | 10 | In this case, we're using a new 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 `ExpensiveComputationComponent` is the _same_ function every time. We can use `useCallback` to make sure that React is handing _the same fibonacci_ to `ExpensiveComputationComponent` every time so it passes its `React.memo` check every single time. Now it's only if `count` changes will it actually re-render (as evidenced by the time.) 11 | 12 | Try removing the useCallback call and see if you get the the count to 40+ that the page crawls as it updates every second. 13 | 14 | [callback]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/Callback.js 15 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/H-uselayouteffect.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useLayoutEffect" 3 | description: "" 4 | --- 5 | 6 | [Component][layout-effect] 7 | 8 | `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 `useLayout` runs at the same time as `componentDidMount` and `componentDidUpdate` whereas `useEffect` is scheduled after. This should be a temporary fix. 9 | 10 | 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. 11 | 12 | [layout-effect]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/LayoutEffect.js 13 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/I-useimperativehandle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useImperativeHandle" 3 | description: "" 4 | --- 5 | 6 | [Component][imperative-handle] 7 | 8 | Here's one you will likely never directly use but you may use libraries that use it for you. We're going to use it in conjunction with another feature called `forwardRef` that again, you probably won't use but libraries will use on your behalf. Let's explain first what it does using the example and then we'll explain the moving parts. 9 | 10 | In the example above, whenever you have an _invalid_ form, it will immediately focus the the first field that's invalid. If you look at the code, `ElaborateInput` is a child element so the parent component shouldn't have any access to the input contained inside the component. Those components are black boxes to their parents. All they can do is pass in props. So how do we accomplish it then? 11 | 12 | The first thing we use is `useImperativeHandle`. This allows us to customize methods on an object that is made available to the parents via the `useRef` API. Inside `ElaborateInput` we have two refs: one thate is the one that will be provided by the parent, forwarded through by wrapping the `ElaborateInput` component in a `forwardRef` call which will ten provide that second `ref` parameter in the function call, and then the `inputRef` which is being used to directly access the DOM so we can call `focus` on the DOM node directly. 13 | 14 | From the parent, we assign via `useRef` a ref to each of the `ElaborateInput`s which is then forwarded on each on via the `forwardRef`. Now, on these refs inside the parent component we have those methods that we made inside the child so we can call them when we need to. In this case, we'll calling the focus when the parent knows that the child has an error. 15 | 16 | Again, you probably use this directly but it's good to know it exists. Normally it's better to not use this hook and try to accomplish the same thing via props but sometimes it may be useful to break this one out. 17 | 18 | [imperative-handle]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/ImperativeHandle.js 19 | -------------------------------------------------------------------------------- /lessons/09-hooks-in-depth/J-usedebugvalue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "useDebugValue" 3 | description: "" 4 | --- 5 | 6 | [Component][debug-value] 7 | 8 | Here's another hook that's baked into React that I don't foresee many of you using but I still want you to know it's there. It, like useImperativeHandle, is more built for library authors. 9 | 10 | useDebugValue allows you to surface information from your custom hook into the dev tools. This allows the developer who is consuming your hook (possibly you, possibly your coworker) to have whatever debugging information you choose to surfaced to them. If you're doing a little custom hook for your app (like the breed one we did in the Intro course) this probably isn't necessary. However if you're consuming a library that has hooks (like how react-router-dom has hooks) these can be useful hints to developers. 11 | 12 | Normally you'd just use the developer tools built into the browser but CodeSandbox has the dev tools built directly into it. Just know that normally you'd use the browser extension. 13 | 14 | [debug-value]: https://codesandbox.io/s/github/btholt/react-hooks-examples-v4/tree/main?file=/src/DebugValue.js 15 | -------------------------------------------------------------------------------- /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.0.22 postcss@8.4.6 autoprefixer@10.4.2 16 | ``` 17 | 18 | - Under the hood, Parcel 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 | - Parcel 1 required a lot more effort to get up and running with Tailwind. Luckily with Parcel 2 it just works! 20 | 21 | Okay, now let's get our Tailwind project going. Run `npx tailwindcss init`. Like `tsc init` for TypeScript, this will spit out a basic starting config in tailwind.config.js. Should look like 22 | 23 | ```javascript 24 | module.exports = { 25 | mode: "jit", 26 | content: ["./src/*.{html,js}"], 27 | theme: { 28 | extend: {}, 29 | }, 30 | variants: {}, 31 | plugins: [], 32 | }; 33 | ``` 34 | 35 | Now, let's go modify our `style.css` file. 36 | 37 | ```css 38 | @tailwind base; 39 | @tailwind components; 40 | @tailwind utilities; 41 | ``` 42 | 43 | > 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. 44 | 45 | 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. 46 | 47 | > There's a great Visual Studio Code extension you should install here: [Tailwind CSS IntelliSense][tw]. 48 | 49 | Lastly, we have to create `.postcssrc` in root directory. 50 | 51 | ```json 52 | { 53 | "plugins": { 54 | "autoprefixer": {}, 55 | "tailwindcss": {} 56 | } 57 | } 58 | ``` 59 | 60 | Now if you run your app you should 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. 61 | 62 | [tw]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss 63 | [sc]: https://btholt.github.io/complete-intro-to-react/ 64 | [emotion]: https://btholt.github.io/complete-intro-to-react-v5/emotion 65 | [tailwind]: https://tailwindcss.com/docs 66 | -------------------------------------------------------------------------------- /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.js, put this: 12 | 13 | ```javascript 14 | // the div right inside 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-purple-400 via-pink-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 purple-400 is a purple 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 `SearchParams.js` (we're only doing `SearchParams.js`, I'll leave it to you to fix `Details.js`) 49 | 50 | ```javascript 51 |
52 |
{ 55 | e.preventDefault(); 56 | requestPets(); 57 | }} 58 | > 59 | […] 60 |
61 |
62 | ``` 63 | 64 | - `rounded-lg` is a "large" rounding of the corners i.e. border-radius. 65 | - `shadow-lg` is a "large" border shadow. 66 | - `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. 67 | -------------------------------------------------------------------------------- /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.4.0`. 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 ` { 31 | updateAnimal(e.target.value as Animal); 32 | updateBreed(""); 33 | }} 34 | onBlur={(e) => { 35 | updateAnimal(e.target.value as Animal); 36 | updateBreed(""); 37 | }} 38 | > 39 | { … } 40 | 41 | ``` 42 | 43 | - Occasionally you need to give TypeScript a hint to what it's going to get. That's what `as` for: you're saying I'm sure it's going to be this. 44 | - We didn't have to add all the Animal typings but since we know it's that we may as well. 45 | 46 | > 🏁 [Click here to see the state of the project up until now: typescript-4][step] 47 | 48 | [step]: https://github.com/btholt/citr-v7-project/tree/master/typescript-4 49 | -------------------------------------------------------------------------------- /lessons/14-typescript/J-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 9 | import { FunctionComponent } from "react"; 10 | import { Pet as PetType } from "./APIResponsesTypes"; 11 | 12 | // replace function declaration 13 | const Results: FunctionComponent<{ pets: PetType[] }> = ({ pets }) => {}; 14 | ``` 15 | 16 | - 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. 17 | - 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. 18 | -------------------------------------------------------------------------------- /lessons/14-typescript/K-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 | ```tsx 8 | // Nothing! 9 | ``` 10 | 11 | Because of the rest of the work we did, App needs no changes! Hooray! 🎉 12 | 13 | Last thing: open `index.html` and change the link from `App.js` to `App.tsx` and then you should be good to go! 14 | 15 | 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. 16 | 17 | 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? 18 | 19 | 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. 20 | 21 | Congrats! You finished TypeScript. 22 | 23 | > 🏁 [Click here to see the state of the project up until now: typescript-5][step] 24 | 25 | [step]: https://github.com/btholt/citr-v7-project/tree/master/typescript-5 26 | -------------------------------------------------------------------------------- /lessons/14-typescript/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "pencil-ruler" 3 | } -------------------------------------------------------------------------------- /lessons/15-redux/A-redux.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Redux is a well-known library that does state management for you, very similarly to how we used context. With context, you use the provider and consumer as a sort of portal to skip passing parameters through every component. With Redux, we're taking the state management _out_ of React entirely and moving it to a separate store. 6 | 7 | Why do we have Redux? 8 | 9 | 1. Context used to be a lot worse to use and less useful. This made Redux (or Redux-like) management tools the only option 10 | 1. Redux code is _extremely testable_. This is probably the most compelling reason to use it. Having your state mutation be broken up in such a way to make it easy to test is fantastic. This is also mitigated because we have `useReducer` now. 11 | 1. The debugging story is pretty good. 12 | 13 | So given that we do now have the next context API, how often will I use Redux? Never, I anticipate. I rarely had problems that Redux solved (they exist; I just didn't have them) and the few cases now where I would see myself using Redux I think React's context would cover it. But if Redux speaks to you, do it! Don't let me stop you. It's a great library. Just be cautious. And there are reasons to use it: if you have complex orchestrations of async data, Redux can be immensely useful and I _would_ use it for that. 14 | 15 | Okay, let's get started. React state management is pretty simple: call setState and let React re-render. That's it! Now there's a few steps involved. 16 | 17 | 1. User types in input box 18 | 1. Call action creator to get an action 19 | 1. Dispatch action to Redux 20 | 1. Redux inserts the action into the root reducer 21 | 1. The root reducer delegates that action to the correct reducer 22 | 1. The reducer returns a new state given the old state and the action object 23 | 1. That new state becomes the store's state 24 | 1. React is then called by Redux and told to update 25 | 26 | So what was one step became several. But each step of this is testable, and that's great. And it's explicit and verbose. It's long to follow, but it's an easy breadcrumb trailer to follow when things go awry. So let's start writing it: 27 | 28 | Run `npm install redux@4.1.2 react-redux@7.2.6`. Create store.js and put in it: 29 | 30 | ```javascript 31 | import { createStore } from "redux"; 32 | import reducer from "./reducers"; 33 | 34 | const store = createStore( 35 | reducer, 36 | typeof window === "object" && 37 | typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== "undefined" 38 | ? window.__REDUX_DEVTOOLS_EXTENSION__() 39 | : (f) => f 40 | ); 41 | 42 | export default store; 43 | ``` 44 | 45 | We're including the dev tools middleware (I'll show you at the end.) This is the base of a store: a reducer. A store is just basically a big object with prescribed ways of changing it. So let's go make our first reducer. 46 | -------------------------------------------------------------------------------- /lessons/15-redux/B-reducers.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Make a new folder in src called `reducers`. Create a file called `index.js` in reducers and put: 6 | 7 | ```javascript 8 | import { combineReducers } from "redux"; 9 | import location from "./location"; 10 | 11 | export default combineReducers({ 12 | location, 13 | }); 14 | ``` 15 | 16 | combineReducers is a convenience function from Redux so you don't have to write your own root reducer. You can if you want to; this is just a bit easier. So now we have a root reducer that will delegate all changed to the `location` key to this reducer. So let's go make it. Make a file called `location.js` and put in it: 17 | 18 | ```javascript 19 | export default function location(state = "Seattle, WA", action) { 20 | switch (action.type) { 21 | case "CHANGE_LOCATION": 22 | return action.payload; 23 | default: 24 | return state; 25 | } 26 | } 27 | ``` 28 | 29 | Not very difficult. A reducer takes an old state, an action, and combines those things to make a state. In this case, if the state is `San Francisco, CA` and some calls it with the action `{type: 'CHANGE_LOCATION': payload: 'Salt Lake City, UT' }` then the _new_ state location would be Salt Lake City, UT. 30 | 31 | A reducer must have a default state. In our case, using ES6 default params, we made Seattle, WA our default state. This is how Redux will initialize your store, by calling each of your reducers once to get a default state. 32 | 33 | The shape of the action object is up to you but there is a thing called [Flux Standard Action][fsa] that some people adhere to to make building tools on top of actions easier. I've not used any of those tools but I also don't have a good reason _not_ to use this shape so I do. In sum, make your action shapes be `{ type: <[String, Number], required>, payload: , error: , meta: }`. The type could in theory be a Symbol too but it messes up the dev tools. 34 | 35 | Reducers are synchronous: they cannot be async. They also must be pure with no side-effects. If you call a reducer 10,000,000 times with the same state and action, you should get the same answer on the 10,000,001st time. 36 | 37 | Okay, so now we understand how, once given a state and an action, we can make a reducer. We haven't made nor dispatched those actions yet but we're getting there. Let's make the other reducers. 38 | 39 | theme.js 40 | 41 | ```javascript 42 | export default function theme(state = "darkblue", action) { 43 | switch (action.type) { 44 | case "CHANGE_THEME": 45 | return action.payload; 46 | default: 47 | return state; 48 | } 49 | } 50 | ``` 51 | 52 | animal.js 53 | 54 | ```javascript 55 | export default function animal(state = "", action) { 56 | switch (action.type) { 57 | case "CHANGE_ANIMAL": 58 | return action.payload; 59 | default: 60 | return state; 61 | } 62 | } 63 | ``` 64 | 65 | breed.js 66 | 67 | ```javascript 68 | export default function breed(state = "", action) { 69 | switch (action.type) { 70 | case "CHANGE_BREED": 71 | return action.payload; 72 | case "CHANGE_ANIMAL": 73 | return ""; 74 | default: 75 | return state; 76 | } 77 | } 78 | ``` 79 | 80 | - For this one, any time we issue a change animal action, we want to reset the breed to be nothing selected so we don't allow users to select tabby dogs or poodle cats (which sounds terrifying.) 81 | 82 | index.js 83 | 84 | ```javascript 85 | import { combineReducers } from "redux"; 86 | import location from "./location"; 87 | import theme from "./theme"; 88 | import animal from "./animal"; 89 | import breed from "./breed"; 90 | 91 | export default combineReducers({ 92 | location, 93 | theme, 94 | animal, 95 | breed, 96 | }); 97 | ``` 98 | -------------------------------------------------------------------------------- /lessons/15-redux/C-action-creators.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's go make the action creators. These are the functions that the UI gives to the store to effect change: actions. These functions create actions. 6 | 7 | Create a new folder called actionCreators and put in changeTheme.js 8 | 9 | ```javascript 10 | export default function changeTheme(theme) { 11 | return { type: "CHANGE_THEME", payload: theme }; 12 | } 13 | ``` 14 | 15 | That's it! This one is the simplest form: create an object and return it. Some people will inline these action shapes in their React components. I prefer this because it makes refactors simple. Let's make the other two: 16 | 17 | changeLocation.js 18 | 19 | ```javascript 20 | export default function changeLocation(location) { 21 | return { type: "CHANGE_LOCATION", payload: location }; 22 | } 23 | ``` 24 | 25 | changeAnimal.js 26 | 27 | ```javascript 28 | export default function changeAnimal(location) { 29 | return { type: "CHANGE_ANIMAL", payload: location }; 30 | } 31 | ``` 32 | 33 | changeLocation.js 34 | 35 | ```javascript 36 | export default function changeBreed(location) { 37 | return { type: "CHANGE_BREED", payload: location }; 38 | } 39 | ``` 40 | 41 | That's it for action creators. In previous versions of this course, I taught how to do async actions so [check this out if you want to see that][v4-async]. there are a thousand flavors of how to do async with Redux. The most popular are [redux-observable][ro], [redux-saga][rs], [redux-promise][rp], and [redux-thunk][rt]. I showed how to use redux-thunk because it's simplest: the others are more powerful but more complex. 42 | 43 | [fsa]: https://github.com/redux-utilities/flux-standard-action 44 | [ro]: https://github.com/redux-observable/redux-observable 45 | [rs]: https://redux-saga.js.org/ 46 | [rp]: https://docs.psb.codes/redux-promise-middleware/ 47 | [rt]: https://github.com/reduxjs/redux-thunk 48 | -------------------------------------------------------------------------------- /lessons/15-redux/D-providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Okay, let's go integrate this now where context was being used before. Go to App.js: 6 | 7 | ```javascript 8 | // delete ThemeContext, useState import 9 | 10 | // import 11 | import { Provider } from "react-redux"; 12 | import store from "./store"; 13 | 14 | // delete useState call 15 | // delete ThemeContext 16 | 17 | // wrap app with 18 | […]; 19 | ``` 20 | 21 | Feels nice deleting a lot of code, right? 22 | 23 | Just like context makes your store available anywhere in your app, so does Provider. 24 | 25 | Now that Redux is available everywhere, let's go add it to SearchParams.js 26 | 27 | ```javascript 28 | // replace ThemeContext import 29 | // delete useContext import 30 | import { useSelector } from "react-redux"; 31 | 32 | // replace context and some usestate references 33 | const animal = useSelector((state) => state.animal); 34 | const location = useSelector((state) => state.location); 35 | const breed = useSelector((state) => state.breed); 36 | const theme = useSelector((state) => state.theme); 37 | ``` 38 | 39 | - This is the newer, hooks-based API for react-redux. This allows you to provide it a selector function that will pluck the bit of state you need from Redux. Very clean, I quite like it. We'll see the older, connect-based one here in a bit. 40 | - You may wonder why we don't just do one `useSelector` call here, return the whole state, and destructure that. Each hook is creating its own subscription and `react-redux` internally uses the functions you provide to check to see if your subscription changed. If the result of your selector function changes, it'll notify React to kick off a re-render of this component. Hence it's important that these subscription functions _just_ grab the state that you care about. Otherwise it will kick off needless re-renders any time _any_ state in your app changes. 41 | -------------------------------------------------------------------------------- /lessons/15-redux/E-dispatching-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | We're not quite done here. We got the reading part of Redux done but not the writing. Let's go do that too in SearchParams.js 6 | 7 | ```javascript 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import changeLocation from "./actionCreators/changeLocation"; 10 | import changeTheme from "./actionCreators/changeTheme"; 11 | import changeAnimal from "./actionCreators/changeAnimal"; 12 | import changeBreed from "./actionCreators/changeBreed"; 13 | 14 | // up with other hooks 15 | const dispatch = useDispatch(); 16 | 17 | // change inputs 18 | 19 | dispatch(changeLocation(e.target.value))} 24 | /> 25 | 26 | 32 | 33 | 40 | 41 | 46 | ``` 47 | 48 | - This dispatching is so much nicer than it is with other API 49 | - The `useDispatch` hook gives you back a dispatching function so you can dispatch actions 50 | - That's really it! 51 | 52 | Now we're also using mapDispatchToState which lets us write functions to dispatch actions to Redux. Let's quickly add it to Details.js 53 | 54 | ```javascript 55 | // replace ThemeContext import 56 | import { connect } from "react-redux"; 57 | 58 | // remove all the ThemeContext stuff and the interior function 59 | // replace `theme` with `this.props.theme` for the backgroundColor 60 | 61 | // bottom 62 | const mapStateToProps = ({ theme }) => ({ theme }); 63 | const ReduxWrappedDetails = connect(mapStateToProps)(Details); 64 | 65 | // replace inside WrappedDetails 66 | ; 67 | ``` 68 | 69 | Now it should work! Redux is a great piece of technology that adds a lot of complexity to your app. Don't add it lightly. I'd say you'd rarely want to start a new project using Redux: hit the problems first and then refactor it in. You just saw how. 70 | -------------------------------------------------------------------------------- /lessons/15-redux/F-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 | > 🏁 [Click here to see the state of the project up until now: redux][step] 17 | 18 | [step]: https://github.com/btholt/citr-v7-project/tree/master/redux 19 | [fox]: https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/ 20 | [chrome]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en 21 | [fem]: https://frontendmasters.com/courses/redux-mobx/ 22 | -------------------------------------------------------------------------------- /lessons/15-redux/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "database" 3 | } -------------------------------------------------------------------------------- /lessons/16-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 Jest tests for the application we just created. 8 | 9 | ## Testing with Jest 10 | 11 | First we'll start with [Jest][jest]. Jest is the testing framework that Facebook puts out. It is not at all tied to React despite both being maintained by Facebook. It's useful for other frameworks and I use it frequently with Node.js applications. 12 | 13 | It's useful to know that Jest is built on top of [Jasmine][jasmine]. Jasmine does the underlying testing part while Jest is the high level runner of the tests. Sometimes it's useful to consult the Jasmine docs too. 14 | 15 | Run `npm install -D jest@27.5.1 @testing-library/react@12.1.3`. 16 | 17 | `@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.) 18 | 19 | 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, Jest assumes all JS files in here are tests. 20 | 21 | We also need to set up Babel to work well on the server (we did something similar for server-side rendering.) Replace your .babelrc with this: 22 | 23 | ```json 24 | { 25 | "presets": [ 26 | [ 27 | "@babel/preset-react", 28 | { 29 | "runtime": "automatic" 30 | } 31 | ], 32 | "@babel/preset-env" 33 | ], 34 | "plugins": ["@babel/plugin-proposal-class-properties"], 35 | "env": { 36 | "test": { 37 | "presets": [ 38 | [ 39 | "@babel/preset-env", 40 | { 41 | "targets": { 42 | "node": "current" 43 | } 44 | } 45 | ] 46 | ] 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | This is just saying when we run Jest (it runs with `NODE_ENV` in `test` mode by default, hence the env name) to transform the code to work for Node.js instead of the browser. 53 | 54 | Let's go add an npm script. In your package.json. 55 | 56 | ```json 57 | "test": "jest", 58 | "test:watch": "jest --watch" 59 | ``` 60 | 61 | Fun trick: if you call it test, npm lets you run that command as just `npm t`. 62 | 63 | We also added `test:watch`. This 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 Jest. 64 | 65 | Now that we've got that going, let's go write a test. 66 | 67 | [jest]: https://jestjs.io 68 | [jasmine]: https://jasmine.github.io/ 69 | [enzyme]: http://airbnb.io/enzyme/ 70 | [istanbul]: https://istanbul.js.org 71 | [res]: https://raw.githubusercontent.com/btholt/complete-intro-to-react-v5/testing/__mocks__/@frontendmasters/res.json 72 | [app]: https://github.com/btholt/citr-v7-project/tree/master/12-portals-and-refs 73 | -------------------------------------------------------------------------------- /lessons/16-testing/B-basic-react-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | Let's write our first test for Pet.js. 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 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.js`. This naming convention is just habit. `Pet.spec.js` 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 | /** 18 | * @jest-environment jsdom 19 | */ 20 | 21 | import { expect, test } from "@jest/globals"; 22 | import { render } from "@testing-library/react"; 23 | import Pet from "../Pet.js"; 24 | 25 | test("displays a default thumbnail", async () => { 26 | const pet = render(); 27 | 28 | const petThumbnail = await pet.findByTestId("thumbnail"); 29 | expect(petThumbnail.src).toContain("none.jpg"); 30 | }); 31 | ``` 32 | 33 | > 🚨 This doesn't work yet. That's intentional. 34 | 35 | We have to identify to Jest what sort of environment we're in for this test. Because it's browser based, we need a browser-like environment which is exactly what jsdom is for. [Read the docs here][jest-env] to learn more about it. 36 | 37 | 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.js` 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. 38 | 39 | 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. 40 | 41 | ```javascript 42 | if (images && images.length) { 43 | hero = images[0]; 44 | } 45 | ``` 46 | 47 | 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. 48 | 49 | ```javascript 50 | // at top 51 | import { StaticRouter } from "react-router-dom"; 52 | 53 | // replace render 54 | const pet = render( 55 | 56 | 57 | 58 | ); 59 | ``` 60 | 61 | > 🚨 This doesn't work yet. That's intentional. 62 | 63 | Now it should pass! 64 | 65 | Let's add one more test case for good measure to test the non-default use case. 66 | 67 | ```javascript 68 | test("displays a non-default thumbnail", async () => { 69 | const pet = render( 70 | 71 | 72 | 73 | ); 74 | 75 | const petThumbnail = await pet.findByTestId("thumbnail"); 76 | expect(petThumbnail.src).toContain("1.jpg"); 77 | }); 78 | ``` 79 | 80 | Bam! Some easy React testing there for you. 81 | 82 | [jest-env]: https://jestjs.io/docs/configuration#testenvironment-string 83 | -------------------------------------------------------------------------------- /lessons/16-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.js. In there put: 11 | 12 | ```javascript 13 | import { expect, test } from "@jest/globals"; 14 | import { render } from "@testing-library/react"; 15 | import Carousel from "../Carousel.js"; 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 | thumb.click(); 29 | 30 | expect(hero.src).toContain(image); 31 | expect(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/16-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.js in our `__tests__` directory. 6 | 7 | ```javascript 8 | /** 9 | * @jest-environment jsdom 10 | */ 11 | 12 | import { expect, test } from "@jest/globals"; 13 | import { render } from "@testing-library/react"; 14 | import useBreedList from "../useBreedList.js"; 15 | 16 | function getBreedList(animal) { 17 | let list; 18 | 19 | function TestComponent() { 20 | list = useBreedList(animal); 21 | return null; 22 | } 23 | 24 | render(); 25 | 26 | return list; 27 | } 28 | 29 | test("gives an empty list with no animal", async () => { 30 | const [breedList, status] = getBreedList(); 31 | expect(breedList).toHaveLength(0); 32 | expect(status).toBe("unloaded"); 33 | }); 34 | ``` 35 | 36 | 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. 37 | 38 | We can make this better though. There's a library called `@testing-library/react-hooks` that hides some of these details from us. Let's run `npm install -D @testing-library/react-hooks@7.0.2` and rewrite our test to look like this. 39 | 40 | ```javascript 41 | /** 42 | * @jest-environment jsdom 43 | */ 44 | 45 | import { expect, test } from "@jest/globals"; 46 | import { renderHook } from "@testing-library/react-hooks"; 47 | import useBreedList from "../useBreedList.js"; 48 | 49 | test("gives an empty list with no animal", async () => { 50 | const { result } = renderHook(() => useBreedList("")); 51 | 52 | const [breedList, status] = result.current; 53 | 54 | expect(breedList).toHaveLength(0); 55 | expect(status).toBe("unloaded"); 56 | }); 57 | ``` 58 | 59 | 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. 60 | -------------------------------------------------------------------------------- /lessons/16-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 Jest called jest-fetch-mock so let's install that. Run `npm install -D jest-fetch-mock@3.0.3`. 6 | 7 | We now need to make it so Jest implements this mock before we run our tests. We can make it run a set up script by putting this in our package.json: 8 | 9 | ```json 10 | { 11 | "jest": { 12 | "automock": false, 13 | "setupFiles": ["./src/setupJest.js"] 14 | } 15 | } 16 | ``` 17 | 18 | Then let's make a file in src called setupJest.js. 19 | 20 | ```javascript 21 | import { enableFetchMocks } from "jest-fetch-mock"; 22 | 23 | enableFetchMocks(); 24 | ``` 25 | 26 | 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. 27 | 28 | Okay, now go back to our useBreedList.test.js and add: 29 | 30 | ```javascript 31 | test("gives back breeds with an animal", async () => { 32 | const breeds = [ 33 | "Havanese", 34 | "Bichon Frise", 35 | "Poodle", 36 | "Maltese", 37 | "Golden Retriever", 38 | "Labrador", 39 | "Husky", 40 | ]; 41 | fetch.mockResponseOnce( 42 | JSON.stringify({ 43 | animal: "dog", 44 | breeds, 45 | }) 46 | ); 47 | const { result, waitForNextUpdate } = renderHook(() => useBreedList("dog")); 48 | 49 | await waitForNextUpdate(); 50 | 51 | const [breedList, status] = result.current; 52 | expect(status).toBe("loaded"); 53 | expect(breedList).toEqual(breeds); 54 | }); 55 | ``` 56 | 57 | The `waitForNextUpdate` 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. 58 | 59 | [openapi]: https://swagger.io/ 60 | -------------------------------------------------------------------------------- /lessons/16-testing/G-istanbul.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "" 3 | --- 4 | 5 | One last very cool trick that Jest has built into it: [Istanbul][istanbul]. Istanbul (which is not [Constantinople][they-might-be-giants]) 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 but now Jest just does it for you. 6 | 7 | Add the following command to your npm scripts: `"test:coverage": "jest --coverage"` and go ahead run `npm run test:coverage` and open the following file in your browser: `open coverage/lcov-report/index.html`. 8 | 9 | Here you can see the four files we've written tests for. One file, `useBreedList` 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 French for software bug). This can help identify gaps in your testing coverage. 10 | 11 | Lastly, add `coverage/` to your `.gitignore` since this shouldn't be checked in. 12 | 13 | > 🏁 [Click here to see the state of the project up until now: testing][step] 14 | 15 | [step]: https://github.com/btholt/citr-v7-project/tree/master/testing 16 | [istanbul]: https://istanbul.js.org/ 17 | [they-might-be-giants]: https://youtu.be/vsQrKZcYtqg 18 | -------------------------------------------------------------------------------- /lessons/16-testing/H-visual-studio-code-extension.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Visual Studio Code has an amazing VS Code extension to use with Jest made by a former TypeScript developer. It makes working Jest a wonderful experience." 3 | --- 4 | 5 | [Please go install this extension][jest-vscode]. 6 | 7 | This extension makes working with Jest 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 Jest snapshots directly from a prompt in VS Code. 17 | - You can see Jest 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 Jest I use it. Highly suggest installing it anytime you're near Jest. 20 | 21 | [jest-vscode]: https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest 22 | -------------------------------------------------------------------------------- /lessons/16-testing/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "vial" 3 | } -------------------------------------------------------------------------------- /lessons/17-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 | [fem]: https://frontendmasters.com/teachers/brian-holt/ 12 | [holtbt]: https://twitter.com/holtbt 13 | [star]: https://github.com/btholt/complete-intro-to-react-v7 14 | -------------------------------------------------------------------------------- /lessons/17-end-of-intermediate/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "stopwatch" 3 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const course = require("./course.json"); 2 | const BASE_URL = course?.productionBaseUrl || ""; 3 | 4 | module.exports = { 5 | basePath: BASE_URL, 6 | env: { 7 | ROOT: __dirname, 8 | BASE_URL, 9 | }, 10 | async redirects() { 11 | if (BASE_URL) { 12 | return [ 13 | { 14 | source: "/", 15 | destination: BASE_URL, 16 | basePath: false, 17 | permanent: false, 18 | }, 19 | ]; 20 | } 21 | return []; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "(CC-BY-NC-4.0 OR Apache-2.0)", 4 | "author": "Brian Holt ", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next build && next export", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-free": "^5.15.4", 13 | "gray-matter": "^4.0.3", 14 | "highlight.js": "^11.4.0", 15 | "marked": "^4.0.9", 16 | "next": "^12.0.7", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "title-case": "^3.0.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@fortawesome/fontawesome-free/js/all.js"; 2 | 3 | import "highlight.js/styles/a11y-light.css"; 4 | import "../styles/variables.css"; 5 | import "../styles/footer.css"; 6 | import "../styles/courses.css"; 7 | 8 | import Layout from "../components/layout"; 9 | 10 | // TODO favicons 11 | 12 | export default function App({ Component, pageProps }) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /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 | 29 | 35 | 41 | 46 | 47 |
48 |
49 |
50 |
51 |

{courseInfo.title}

52 |

{courseInfo.subtitle}

53 |
54 |
55 | author image 60 |
61 |
62 |
{courseInfo.author.name}
63 |
{courseInfo.author.company}
64 |
65 |
66 |
67 |
68 |
69 | course icon 73 |
74 |
75 | {courseInfo.frontendMastersLink ? ( 76 |
77 | Watch on Frontend Masters 78 | 79 | ) : null} 80 |
81 |

Table of Contents

82 |
83 |
    84 | {sections.map((section) => ( 85 |
  1. 86 |
    87 |
    88 | 89 |
    90 |
    91 |

    {section.title}

    92 |
      93 | {section.lessons.map((lesson) => ( 94 |
    1. 95 | {lesson.title} 96 |
    2. 97 | ))} 98 |
    99 |
    100 | 101 |
    102 |
  2. 103 | ))} 104 |
105 |
106 |
107 |
108 | 109 | ); 110 | } 111 | 112 | export async function getStaticProps() { 113 | const sections = await getLessons(); 114 | return { 115 | props: { 116 | sections, 117 | }, 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /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 const meta = (routeData) => { 9 | // return { 10 | // title: `${routeData.data.section} – ${routeData.data.title}`, 11 | // description: routeData.data.attributes.description, 12 | // }; 13 | // }; 14 | 15 | export default function LessonSlug({ post }) { 16 | const courseInfo = getCourseConfig(); 17 | const [_, setHeader] = useContext(Context); 18 | useEffect(() => { 19 | setHeader({ 20 | section: post.section, 21 | title: post.title, 22 | icon: post.icon, 23 | }); 24 | return () => setHeader({}); 25 | }, []); 26 | 27 | const title = post.title 28 | ? `${post.title} – ${courseInfo.title}` 29 | : courseInfo.title; 30 | const description = post.description 31 | ? post.description 32 | : courseInfo.description; 33 | 34 | return ( 35 | <> 36 | 37 | {title} 38 | 39 | {/* */} 40 | 41 | 42 | 46 | 47 | 48 |
49 |
50 |
54 |
55 | {post.prevSlug ? ( 56 | 57 | ← Previous 58 | 59 | ) : null} 60 | {post.nextSlug ? ( 61 | 62 | Next → 63 | 64 | ) : null} 65 |
66 |
67 | 68 |
69 | 70 | ); 71 | } 72 | 73 | export async function getStaticProps({ params }) { 74 | const post = await getLesson(params.section, params.slug); 75 | return { 76 | props: { 77 | post, 78 | }, 79 | }; 80 | } 81 | 82 | export async function getStaticPaths() { 83 | const sections = await getLessons(); 84 | const lessons = sections.map((section) => section.lessons); 85 | const slugs = lessons.flat().map((lesson) => lesson.fullSlug); 86 | 87 | return { paths: slugs, fallback: false }; 88 | } 89 | -------------------------------------------------------------------------------- /public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/.nojekyll -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/favicon.ico -------------------------------------------------------------------------------- /public/images/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/images/author.jpg -------------------------------------------------------------------------------- /public/images/brian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/images/brian.jpg -------------------------------------------------------------------------------- /public/images/course-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/public/images/course-icon.png -------------------------------------------------------------------------------- /public/images/social-share-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v7/0bfabf7d2d87bb13633de8c34e855d2aee3e0817/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: #aaa; 3 | --secondary: #333; 4 | --highlight: #333; 5 | 6 | --text-header: var(--primary); 7 | --text-main-headers: var(--highlight); 8 | --text-links: #007bff; 9 | --text-footer: #333; 10 | 11 | --bg-main: white; 12 | --bg-dots: var(--highlight); 13 | --bg-lesson: white; 14 | --bg-preface: var(--primary); 15 | 16 | --nav-buttons: var(--highlight); 17 | --nav-buttons-text: white; 18 | 19 | --corner-active: var(--highlight); 20 | --corner-inactive: #f4f4f4; 21 | --icons: var(--highlight); 22 | 23 | --emphasized-bg: #dce8ff; 24 | --emphasized-border: #aab6d2; 25 | } 26 | --------------------------------------------------------------------------------