├── .all-contributorsrc ├── .eslintignore ├── .gitattributes ├── .github └── workflows │ └── validate.yml ├── .gitignore ├── .gitpod.yml ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.kcd.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── craco.config.js ├── docker-compose.yml ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── manifest.json └── mockServiceWorker.js ├── sandbox.config.json ├── scripts ├── diff.js ├── fix-links ├── pre-commit.js ├── pre-push.js ├── setup.js └── update-deps ├── setup.js └── src ├── __tests__ ├── 05.js ├── 06.extra-2.js ├── 06.extra-3.js ├── 06.js └── 07.js ├── box-styles.css ├── exercise ├── 01.html ├── 01.md ├── 02.html ├── 02.md ├── 03.html ├── 03.md ├── 04.html ├── 04.md ├── 05.js ├── 05.md ├── 06.js ├── 06.md ├── 07.js └── 07.md ├── final ├── 01.extra-1.html ├── 01.html ├── 02.extra-1.html ├── 02.html ├── 03.extra-1.html ├── 03.extra-2.html ├── 03.html ├── 04.extra-1.html ├── 04.extra-2.html ├── 04.extra-3.html ├── 04.extra-4.html ├── 04.extra-5.html ├── 04.html ├── 05.extra-1.js ├── 05.extra-2.js ├── 05.js ├── 06.extra-1.js ├── 06.extra-2.js ├── 06.extra-3.js ├── 06.js ├── 07.extra-1.js └── 07.js ├── index.js ├── setupTests.js └── styles.css /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-fundamentals", 3 | "projectOwner": "kentcdodds", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "repoHost": "https://github.com", 11 | "contributorsPerLine": 7, 12 | "contributors": [ 13 | { 14 | "login": "kentcdodds", 15 | "name": "Kent C. Dodds", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 17 | "profile": "https://kentcdodds.com", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "infra", 22 | "test" 23 | ] 24 | }, 25 | { 26 | "login": "jdorfman", 27 | "name": "Justin Dorfman", 28 | "avatar_url": "https://avatars1.githubusercontent.com/u/398230?v=4", 29 | "profile": "https://stackshare.io/jdorfman/decisions", 30 | "contributions": [ 31 | "fundingFinding" 32 | ] 33 | }, 34 | { 35 | "login": "benmvp", 36 | "name": "Ben Ilegbodu", 37 | "avatar_url": "https://avatars3.githubusercontent.com/u/5714478?v=4", 38 | "profile": "http://www.benmvp.com", 39 | "contributions": [ 40 | "doc" 41 | ] 42 | }, 43 | { 44 | "login": "belcherj", 45 | "name": "Jonathan Belcher", 46 | "avatar_url": "https://avatars1.githubusercontent.com/u/6817400?v=4", 47 | "profile": "https://github.com/belcherj", 48 | "contributions": [ 49 | "doc" 50 | ] 51 | }, 52 | { 53 | "login": "rhefner", 54 | "name": "Richard Hefner", 55 | "avatar_url": "https://avatars1.githubusercontent.com/u/8144799?v=4", 56 | "profile": "https://github.com/rhefner", 57 | "contributions": [ 58 | "code" 59 | ] 60 | }, 61 | { 62 | "login": "zacjones93", 63 | "name": "Zac Jones", 64 | "avatar_url": "https://avatars2.githubusercontent.com/u/6188161?v=4", 65 | "profile": "https://zacjones.io", 66 | "contributions": [ 67 | "doc" 68 | ] 69 | }, 70 | { 71 | "login": "rbusquet", 72 | "name": "Ricardo Busquet", 73 | "avatar_url": "https://avatars1.githubusercontent.com/u/7198302?v=4", 74 | "profile": "https://ricardobusquet.com", 75 | "contributions": [ 76 | "doc" 77 | ] 78 | }, 79 | { 80 | "login": "sleepyArpan", 81 | "name": "Arpan Chattopadhyay", 82 | "avatar_url": "https://avatars3.githubusercontent.com/u/50901152?v=4", 83 | "profile": "https://github.com/sleepyArpan", 84 | "contributions": [ 85 | "doc" 86 | ] 87 | }, 88 | { 89 | "login": "marcosvega91", 90 | "name": "Marco Moretti", 91 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", 92 | "profile": "https://github.com/marcosvega91", 93 | "contributions": [ 94 | "code" 95 | ] 96 | }, 97 | { 98 | "login": "cindywu", 99 | "name": "cindy", 100 | "avatar_url": "https://avatars3.githubusercontent.com/u/1177031?v=4", 101 | "profile": "http://cindywu.org", 102 | "contributions": [ 103 | "doc" 104 | ] 105 | }, 106 | { 107 | "login": "lifeiscontent", 108 | "name": "Aaron Reisman", 109 | "avatar_url": "https://avatars3.githubusercontent.com/u/180963?v=4", 110 | "profile": "https://lifeiscontent.net/", 111 | "contributions": [ 112 | "doc" 113 | ] 114 | }, 115 | { 116 | "login": "JacobMGEvans", 117 | "name": "Jacob M-G Evans", 118 | "avatar_url": "https://avatars1.githubusercontent.com/u/27247160?v=4", 119 | "profile": "https://dev.to/jacobmgevans", 120 | "contributions": [ 121 | "review" 122 | ] 123 | }, 124 | { 125 | "login": "jsehull", 126 | "name": "Jesse Hull", 127 | "avatar_url": "https://avatars1.githubusercontent.com/u/9935383?v=4", 128 | "profile": "https://jsehull.com", 129 | "contributions": [ 130 | "doc" 131 | ] 132 | }, 133 | { 134 | "login": "tcaraccia-riv", 135 | "name": "Tomas Caraccia", 136 | "avatar_url": "https://avatars2.githubusercontent.com/u/64477810?v=4", 137 | "profile": "https://github.com/tcaraccia-riv", 138 | "contributions": [ 139 | "doc" 140 | ] 141 | }, 142 | { 143 | "login": "vasilii-kovalev", 144 | "name": "Vasilii Kovalev", 145 | "avatar_url": "https://avatars0.githubusercontent.com/u/10310491?v=4", 146 | "profile": "https://vk.com/vasilii_kovalev", 147 | "contributions": [ 148 | "code" 149 | ] 150 | }, 151 | { 152 | "login": "FelixGeelhaar", 153 | "name": "Felix Geelhaar", 154 | "avatar_url": "https://avatars0.githubusercontent.com/u/6020564?v=4", 155 | "profile": "https://github.com/FelixGeelhaar", 156 | "contributions": [ 157 | "doc" 158 | ] 159 | }, 160 | { 161 | "login": "apolakipso", 162 | "name": "Apola Kipso", 163 | "avatar_url": "https://avatars2.githubusercontent.com/u/494674?v=4", 164 | "profile": "https://twitter.com/apolakipso", 165 | "contributions": [ 166 | "doc" 167 | ] 168 | }, 169 | { 170 | "login": "dcgoodwin2112", 171 | "name": "dcgoodwin2112", 172 | "avatar_url": "https://avatars1.githubusercontent.com/u/4554388?v=4", 173 | "profile": "https://github.com/dcgoodwin2112", 174 | "contributions": [ 175 | "bug" 176 | ] 177 | }, 178 | { 179 | "login": "PritamSangani", 180 | "name": "Pritam Sangani", 181 | "avatar_url": "https://avatars3.githubusercontent.com/u/22857896?v=4", 182 | "profile": "https://www.linkedin.com/in/pritamsangani/", 183 | "contributions": [ 184 | "code" 185 | ] 186 | }, 187 | { 188 | "login": "rchinerman", 189 | "name": "Ryan Hinerman", 190 | "avatar_url": "https://avatars3.githubusercontent.com/u/17489675?v=4", 191 | "profile": "https://github.com/rchinerman", 192 | "contributions": [ 193 | "doc", 194 | "code" 195 | ] 196 | }, 197 | { 198 | "login": "Marcoj776", 199 | "name": "Marco", 200 | "avatar_url": "https://avatars0.githubusercontent.com/u/9052097?v=4", 201 | "profile": "https://github.com/Marcoj776", 202 | "contributions": [ 203 | "bug" 204 | ] 205 | }, 206 | { 207 | "login": "Aprillion", 208 | "name": "Peter Hozák", 209 | "avatar_url": "https://avatars0.githubusercontent.com/u/1087670?v=4", 210 | "profile": "http://peter.hozak.info/", 211 | "contributions": [ 212 | "code" 213 | ] 214 | }, 215 | { 216 | "login": "emzoumpo", 217 | "name": "Emmanouil Zoumpoulakis", 218 | "avatar_url": "https://avatars2.githubusercontent.com/u/2103443?v=4", 219 | "profile": "https://github.com/emzoumpo", 220 | "contributions": [ 221 | "doc" 222 | ] 223 | }, 224 | { 225 | "login": "Navneet-Sahota", 226 | "name": "Navneet Sahota", 227 | "avatar_url": "https://avatars1.githubusercontent.com/u/34404592?v=4", 228 | "profile": "https://navneet-sahota.netlify.com", 229 | "contributions": [ 230 | "doc" 231 | ] 232 | }, 233 | { 234 | "login": "rodrigofuentes", 235 | "name": "Rodrigo Fuentes", 236 | "avatar_url": "https://avatars1.githubusercontent.com/u/7374840?v=4", 237 | "profile": "https://github.com/rodrigofuentes", 238 | "contributions": [ 239 | "doc" 240 | ] 241 | }, 242 | { 243 | "login": "jmagrippis", 244 | "name": "Johnny Magrippis", 245 | "avatar_url": "https://avatars0.githubusercontent.com/u/3502800?v=4", 246 | "profile": "https://magrippis.com", 247 | "contributions": [ 248 | "code" 249 | ] 250 | }, 251 | { 252 | "login": "coderosh", 253 | "name": "Roshan Acharya", 254 | "avatar_url": "https://avatars2.githubusercontent.com/u/56434316?v=4", 255 | "profile": "http://acharyaroshan.com.np", 256 | "contributions": [ 257 | "doc" 258 | ] 259 | }, 260 | { 261 | "login": "decisa", 262 | "name": "Art Telesh", 263 | "avatar_url": "https://avatars0.githubusercontent.com/u/35339760?v=4", 264 | "profile": "https://github.com/decisa", 265 | "contributions": [ 266 | "doc", 267 | "code" 268 | ] 269 | }, 270 | { 271 | "login": "merodiro", 272 | "name": "Amr A.Mohammed", 273 | "avatar_url": "https://avatars1.githubusercontent.com/u/17033502?v=4", 274 | "profile": "https://github.com/merodiro", 275 | "contributions": [ 276 | "ideas" 277 | ] 278 | }, 279 | { 280 | "login": "DRS90", 281 | "name": "Douglas", 282 | "avatar_url": "https://avatars1.githubusercontent.com/u/22821570?v=4", 283 | "profile": "https://github.com/DRS90", 284 | "contributions": [ 285 | "doc" 286 | ] 287 | }, 288 | { 289 | "login": "allstargaurav", 290 | "name": "Gaurav", 291 | "avatar_url": "https://avatars3.githubusercontent.com/u/24932097?v=4", 292 | "profile": "https://github.com/allstargaurav", 293 | "contributions": [ 294 | "code" 295 | ] 296 | }, 297 | { 298 | "login": "LauraOneasca", 299 | "name": "LauraOneasca", 300 | "avatar_url": "https://avatars2.githubusercontent.com/u/31212753?v=4", 301 | "profile": "https://github.com/LauraOneasca", 302 | "contributions": [ 303 | "doc" 304 | ] 305 | }, 306 | { 307 | "login": "MichaelDeBoey", 308 | "name": "Michaël De Boey", 309 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 310 | "profile": "https://michaeldeboey.be", 311 | "contributions": [ 312 | "code" 313 | ] 314 | }, 315 | { 316 | "login": "Xiphe", 317 | "name": "Hannes Diercks", 318 | "avatar_url": "https://avatars1.githubusercontent.com/u/911218?v=4", 319 | "profile": "http://xiphe.net", 320 | "contributions": [ 321 | "test" 322 | ] 323 | }, 324 | { 325 | "login": "denno020", 326 | "name": "Luke", 327 | "avatar_url": "https://avatars2.githubusercontent.com/u/2059313?v=4", 328 | "profile": "https://github.com/denno020", 329 | "contributions": [ 330 | "bug" 331 | ] 332 | }, 333 | { 334 | "login": "tonidezman", 335 | "name": "Toni Dezman", 336 | "avatar_url": "https://avatars0.githubusercontent.com/u/11177270?v=4", 337 | "profile": "https://tonidezman.github.io/", 338 | "contributions": [ 339 | "code" 340 | ] 341 | }, 342 | { 343 | "login": "bobbywarner", 344 | "name": "Bobby Warner", 345 | "avatar_url": "https://avatars0.githubusercontent.com/u/554961?v=4", 346 | "profile": "http://bobbywarner.com", 347 | "contributions": [ 348 | "code" 349 | ] 350 | }, 351 | { 352 | "login": "PunkSage", 353 | "name": "Konrad Szałwiński", 354 | "avatar_url": "https://avatars2.githubusercontent.com/u/1321225?v=4", 355 | "profile": "https://github.com/PunkSage", 356 | "contributions": [ 357 | "code" 358 | ] 359 | }, 360 | { 361 | "login": "yaseenkadir", 362 | "name": "Yaseen Kadir", 363 | "avatar_url": "https://avatars1.githubusercontent.com/u/8746946?v=4", 364 | "profile": "https://au.linkedin.com/pub/yaseen-kadir/102/99a/49a", 365 | "contributions": [ 366 | "bug" 367 | ] 368 | }, 369 | { 370 | "login": "viglucci", 371 | "name": "Kevin Viglucci", 372 | "avatar_url": "https://avatars0.githubusercontent.com/u/6305490?v=4", 373 | "profile": "http://viglucci.io", 374 | "contributions": [ 375 | "doc" 376 | ] 377 | }, 378 | { 379 | "login": "iliyan-trifonov", 380 | "name": "Iliyan Trifonov", 381 | "avatar_url": "https://avatars.githubusercontent.com/u/2099265?v=4", 382 | "profile": "http://www.iliyan-trifonov.com", 383 | "contributions": [ 384 | "doc" 385 | ] 386 | }, 387 | { 388 | "login": "oscard0m", 389 | "name": "Oscar Dominguez", 390 | "avatar_url": "https://avatars.githubusercontent.com/u/2574275?v=4", 391 | "profile": "https://dev.to/oscardom", 392 | "contributions": [ 393 | "doc" 394 | ] 395 | }, 396 | { 397 | "login": "aaronccasanova", 398 | "name": "Aaron Casanova", 399 | "avatar_url": "https://avatars.githubusercontent.com/u/32409546?v=4", 400 | "profile": "http://cpcomponents.com", 401 | "contributions": [ 402 | "code" 403 | ] 404 | }, 405 | { 406 | "login": "kylegach", 407 | "name": "Kyle Gach", 408 | "avatar_url": "https://avatars.githubusercontent.com/u/486540?v=4", 409 | "profile": "https://kylegach.com", 410 | "contributions": [ 411 | "doc" 412 | ] 413 | }, 414 | { 415 | "login": "antdke", 416 | "name": "Anthony Diké", 417 | "avatar_url": "https://avatars.githubusercontent.com/u/22419667?v=4", 418 | "profile": "https://antdke.co", 419 | "contributions": [ 420 | "doc" 421 | ] 422 | }, 423 | { 424 | "login": "m4ttsch", 425 | "name": "Matt Schlenker", 426 | "avatar_url": "https://avatars.githubusercontent.com/u/19544466?v=4", 427 | "profile": "https://www.omscs-notes.com", 428 | "contributions": [ 429 | "doc" 430 | ] 431 | }, 432 | { 433 | "login": "Siemik", 434 | "name": "Jakub", 435 | "avatar_url": "https://avatars.githubusercontent.com/u/45874801?v=4", 436 | "profile": "https://github.com/Siemik", 437 | "contributions": [ 438 | "doc" 439 | ] 440 | }, 441 | { 442 | "login": "debone", 443 | "name": "Victor Debone", 444 | "avatar_url": "https://avatars.githubusercontent.com/u/763457?v=4", 445 | "profile": "https://debone.com.br", 446 | "contributions": [ 447 | "doc" 448 | ] 449 | }, 450 | { 451 | "login": "lifeparticle", 452 | "name": "Mahbub Zaman", 453 | "avatar_url": "https://avatars.githubusercontent.com/u/1612112?v=4", 454 | "profile": "https://medium.com/@lifeparticle", 455 | "contributions": [ 456 | "doc" 457 | ] 458 | }, 459 | { 460 | "login": "mokajima", 461 | "name": "Misaki Okajima", 462 | "avatar_url": "https://avatars.githubusercontent.com/u/10166985?v=4", 463 | "profile": "https://mokajima.com/", 464 | "contributions": [ 465 | "doc" 466 | ] 467 | }, 468 | { 469 | "login": "marioleed", 470 | "name": "Mario Sannum", 471 | "avatar_url": "https://avatars.githubusercontent.com/u/1763448?v=4", 472 | "profile": "https://github.com/marioleed", 473 | "contributions": [ 474 | "code" 475 | ] 476 | }, 477 | { 478 | "login": "octokatherine", 479 | "name": "Katherine Peterson", 480 | "avatar_url": "https://avatars.githubusercontent.com/u/49968061?v=4", 481 | "profile": "https://github.com/octokatherine", 482 | "contributions": [ 483 | "doc" 484 | ] 485 | }, 486 | { 487 | "login": "alexcwatt", 488 | "name": "Alex Watt", 489 | "avatar_url": "https://avatars.githubusercontent.com/u/494201?v=4", 490 | "profile": "https://alexcwatt.com/", 491 | "contributions": [ 492 | "doc" 493 | ] 494 | }, 495 | { 496 | "login": "nedredmond", 497 | "name": "Ned Redmond", 498 | "avatar_url": "https://avatars.githubusercontent.com/u/23404711?v=4", 499 | "profile": "https://github.com/nedredmond", 500 | "contributions": [ 501 | "test" 502 | ] 503 | }, 504 | { 505 | "login": "ozadari5", 506 | "name": "Oz Adari", 507 | "avatar_url": "https://avatars.githubusercontent.com/u/92675396?v=4", 508 | "profile": "https://github.com/ozadari5", 509 | "contributions": [ 510 | "doc" 511 | ] 512 | }, 513 | { 514 | "login": "cRAN-cg", 515 | "name": "Chiranjeev Gupta", 516 | "avatar_url": "https://avatars.githubusercontent.com/u/8614844?v=4", 517 | "profile": "https://github.com/cRAN-cg", 518 | "contributions": [ 519 | "doc" 520 | ] 521 | }, 522 | { 523 | "login": "sunnatganiev", 524 | "name": "Sunnatullo Ganiev", 525 | "avatar_url": "https://avatars.githubusercontent.com/u/38115176?v=4", 526 | "profile": "https://github.com/sunnatganiev", 527 | "contributions": [ 528 | "doc" 529 | ] 530 | }, 531 | { 532 | "login": "jcat4", 533 | "name": "Joey Cardosi", 534 | "avatar_url": "https://avatars.githubusercontent.com/u/7866287?v=4", 535 | "profile": "https://github.com/jcat4", 536 | "contributions": [ 537 | "doc" 538 | ] 539 | }, 540 | { 541 | "login": "Havock94", 542 | "name": "Luca", 543 | "avatar_url": "https://avatars.githubusercontent.com/u/7635248?v=4", 544 | "profile": "https://github.com/Havock94", 545 | "contributions": [ 546 | "doc" 547 | ] 548 | } 549 | ], 550 | "skipCi": true, 551 | "commitConvention": "angular" 552 | } 553 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | pull_request: 7 | branches: 8 | - 'main' 9 | jobs: 10 | setup: 11 | # ignore all-contributors PRs 12 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: ⬇️ Checkout repo 19 | uses: actions/checkout@v3 20 | 21 | - name: ⎔ Setup node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | 26 | - name: npm 8 27 | run: npm install --global npm@8 28 | 29 | - name: ▶️ Run setup script 30 | run: npm run setup 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | build 5 | .idea/ 6 | .vscode/ 7 | .eslintcache 8 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: App 3 | init: npm install 4 | command: npm run start 5 | openMode: split-left 6 | 7 | - name: Test 8 | command: npm run test 9 | openMode: split-right 10 | 11 | - name: Set up email 12 | command: | 13 | clear 14 | printf "\n\n\n" 15 | printf "\u001b[36;1mAutofilling Email\u001b[0m\n" 16 | printf "\u001b[2;1mEach exercise comes with a elaboration form to help your retention. Providing your email now will mean you don't have to provide it each time you fill out the form.\u001b[0m\n" 17 | 18 | npx "https://gist.github.com/kentcdodds/2d44448a8997b9964b1be44cd294d1f5" \ 19 | && exit 0 20 | 21 | vscode: 22 | extensions: 23 | - VisualStudioExptTeam.vscodeintellicode 24 | - dbaeumer.vscode-eslint 25 | - formulahendry.auto-rename-tag 26 | - esbenp.prettier-vscode 27 | - ms-azuretools.vscode-docker 28 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=true 3 | yes=true 4 | legacy-peer-deps=true 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "always", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "all", 17 | "useTabs": false 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "formulahendry.auto-rename-tag", 6 | "VisualStudioExptTeam.vscodeintellicode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.kcd.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.detectIndentation": true, 5 | "editor.fontFamily": "'Dank Mono', Menlo, Monaco, 'Courier New', monospace", 6 | "editor.fontLigatures": false, 7 | "editor.rulers": [80], 8 | "editor.snippetSuggestions": "top", 9 | "editor.wordBasedSuggestions": false, 10 | "editor.suggest.localityBonus": true, 11 | "editor.acceptSuggestionOnCommitCharacter": false, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.suggestSelection": "recentlyUsed", 15 | "editor.suggest.showKeywords": false 16 | }, 17 | "editor.renderWhitespace": "boundary", 18 | "files.defaultLanguage": "{activeEditorLanguage}", 19 | "javascript.validate.enable": false, 20 | "search.exclude": { 21 | "**/node_modules": true, 22 | "**/bower_components": true, 23 | "**/coverage": true, 24 | "**/dist": true, 25 | "**/build": true, 26 | "**/.build": true, 27 | "**/.gh-pages": true 28 | }, 29 | "editor.codeActionsOnSave": { 30 | "source.fixAll.eslint": false 31 | }, 32 | "eslint.validate": [ 33 | "javascript", 34 | "javascriptreact", 35 | "typescript", 36 | "typescriptreact" 37 | ], 38 | "eslint.options": { 39 | "env": { 40 | "browser": true, 41 | "jest/globals": true, 42 | "es6": true 43 | }, 44 | "parserOptions": { 45 | "ecmaVersion": 2019, 46 | "sourceType": "module", 47 | "ecmaFeatures": { 48 | "jsx": true 49 | } 50 | }, 51 | "rules": { 52 | "no-debugger": "off" 53 | } 54 | }, 55 | "workbench.colorTheme": "Night Owl", 56 | "workbench.iconTheme": "material-icon-theme", 57 | "workbench.editorAssociations": { 58 | "*.md": "vscode.markdown.preview.editor" 59 | }, 60 | "breadcrumbs.enabled": true, 61 | "grunt.autoDetect": "off", 62 | "gulp.autoDetect": "off", 63 | "npm.runSilent": true, 64 | "explorer.confirmDragAndDrop": false, 65 | "editor.formatOnPaste": false, 66 | "editor.cursorSmoothCaretAnimation": true, 67 | "editor.smoothScrolling": true, 68 | "php.suggest.basic": false 69 | } 70 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please refer to [kentcdodds.com/conduct/](https://kentcdodds.com/conduct/) 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make 15 | > pull requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/react-fundamentals.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` 25 | > branch to use the upstream main branch whenever you run `git pull`. Then you 26 | > can make all of your pull request branches based on this `main` branch. 27 | > Whenever you want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Help needed 30 | 31 | Please checkout the [the open issues][issues] 32 | 33 | Also, please watch the repo and respond to questions/bug reports/feature 34 | requests! Thanks! 35 | 36 | [egghead]: 37 | https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 38 | [issues]: https://github.com/kentcdodds/react-fundamentals/issues 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN NO_EMAIL_AUTOFILL=true node setup 6 | 7 | CMD ["npm", "start"] 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This material is available for private, non-commercial use under the 2 | [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you 3 | would like to use this material to conduct your own workshop, please contact me 4 | at me@kentcdodds.com 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

React Fundamentals 🚀 EpicReact.Dev

3 | 4 | Learn the foundational concepts necessary for building 5 | React applications and libraries 6 | 7 |

8 | Learn everything you need to be effective with the fundamental building 9 | block of React applications. When you're finished, you'll be prepared to 10 | create React components to build excellent experiences for your app's users. 11 |

12 | 13 | 14 | Learn React from Start to Finish 18 | 19 |
20 | 21 |
22 | 23 | 24 | [![Build Status][build-badge]][build] 25 | [![GPL 3.0 License][license-badge]][license] 26 | [![All Contributors][all-contributors-badge]](#contributors-) 27 | [![Code of Conduct][coc-badge]][coc] 28 | [![Gitpod ready-to-code][gitpod-badge]](https://gitpod.io/#https://github.com/kentcdodds/react-fundamentals) 29 | 30 | 31 | ## Prerequisites 32 | 33 | - Read through 34 | ["JavaScript to Know for React"](https://kentcdodds.com/blog/javascript-to-know-for-react) 35 | - Install the React DevTools 36 | ([Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) 37 | (recommended), 38 | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)) 39 | 40 | > NOTE: The EpicReact.dev videos were recorded with React version ^16.13 and all 41 | > material in this repo has been updated to React version ^18. Differences are 42 | > minor and any relevant differences are noted in the instructions. 43 | 44 | ## Quick start 45 | 46 | It's recommended you run everything in the same environment you work in every 47 | day, but if you don't want to set up the repository locally, you can get started 48 | in one click with [Gitpod](https://gitpod.io), 49 | [CodeSandbox](https://codesandbox.io/s/github/kentcdodds/react-fundamentals), or 50 | by following the [video demo](https://www.youtube.com/watch?v=gCoVJm3hGk4) 51 | instructions for [GitHub Codespaces](https://github.com/features/codespaces). 52 | 53 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/kentcdodds/react-fundamentals) 54 | 55 | For a local development environment, follow the instructions below 56 | 57 | ## System Requirements 58 | 59 | - [git][git] v2.13 or greater 60 | - [NodeJS][node] `14 || 16 || 18` 61 | - [npm][npm] v8.16.0 or greater 62 | 63 | All of these must be available in your `PATH`. To verify things are set up 64 | properly, you can run this: 65 | 66 | ```shell 67 | git --version 68 | node --version 69 | npm --version 70 | ``` 71 | 72 | If you have trouble with any of these, learn more about the PATH environment 73 | variable and how to fix it here for [windows][win-path] or 74 | [mac/linux][mac-path]. 75 | 76 | ## Setup 77 | 78 | > If you want to commit and push your work as you go, you'll want to 79 | > [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) 80 | > first and then clone your fork rather than this repo directly. 81 | 82 | After you've made sure to have the correct things (and versions) installed, you 83 | should be able to just run a few commands to get set up: 84 | 85 | ```shell 86 | git clone https://github.com/kentcdodds/react-fundamentals.git 87 | cd react-fundamentals 88 | node setup 89 | ``` 90 | 91 | This may take a few minutes. **It will ask you for your email.** This is 92 | optional and just automatically adds your email to the links in the project to 93 | make filling out some forms easier. 94 | 95 | A few common issues during `node setup` have involved PATH variables (above 96 | links or [here](https://github.com/kentcdodds/react-fundamentals/issues/27)), 97 | reinstalling git, node, or npm, and clearing npm caches. 98 | 99 | If you get any errors, please read through them and see if you can find out what 100 | the problem is. If you can't work it out on your own then please [file an 101 | issue][issue] and provide _all_ the output from the commands you ran (even if 102 | it's a lot). 103 | 104 | If you can't get the setup script to work, then just make sure you have the 105 | right versions of the requirements listed above, and run the following commands: 106 | 107 | ```shell 108 | npm install 109 | npm run validate 110 | ``` 111 | 112 | If you are still unable to fix issues and you know how to use Docker 🐳 you can 113 | setup the project with the following command: 114 | 115 | ```shell 116 | docker-compose up 117 | ``` 118 | 119 | ## Running the app 120 | 121 | To get the app up and running (and really see if it worked), run: 122 | 123 | ```shell 124 | npm start 125 | ``` 126 | 127 | This should start up your browser. If you're familiar, this is a standard 128 | [react-scripts](https://create-react-app.dev/) application. 129 | 130 | You can also open 131 | [the deployment of the app on Netlify](https://react-fundamentals.netlify.app/). 132 | 133 | ## Running the tests 134 | 135 | ```shell 136 | npm test 137 | ``` 138 | 139 | This will start [Jest](https://jestjs.io/) in watch mode. Read the output and 140 | play around with it. The tests are there to help you reach the final version, 141 | however _sometimes_ you can accomplish the task and the tests still fail if you 142 | implement things differently than I do in my solution, so don't look to them as 143 | a complete authority. 144 | 145 | ### Exercises 146 | 147 | - `src/exercise/00.md`: Background, Exercise Instructions, Extra Credit 148 | - `src/exercise/00.js`: Exercise with Emoji helpers 149 | - `src/__tests__/00.js`: Tests 150 | - `src/final/00.js`: Final version 151 | - `src/final/00.extra-0.js`: Final version of extra credit 152 | 153 | The purpose of the exercise is **not** for you to work through all the material. 154 | It's intended to get your brain thinking about the right questions to ask me as 155 | _I_ walk through the material. 156 | 157 | ### Helpful Emoji 🐨 💰 💯 📝 🦉 📜 💣 💪 🏁 👨‍💼 🚨 158 | 159 | Each exercise has comments in it to help you get through the exercise. These fun 160 | emoji characters are here to help you. 161 | 162 | - **Kody the Koala** 🐨 will tell you when there's something specific you should 163 | do 164 | - **Marty the Money Bag** 💰 will give you specific tips (and sometimes code) 165 | along the way 166 | - **Hannah the Hundred** 💯 will give you extra challenges you can do if you 167 | finish the exercises early. 168 | - **Nancy the Notepad** 📝 will encourage you to take notes on what you're 169 | learning 170 | - **Olivia the Owl** 🦉 will give you useful tidbits/best practice notes and a 171 | link for elaboration and feedback. 172 | - **Dominic the Document** 📜 will give you links to useful documentation 173 | - **Berry the Bomb** 💣 will be hanging around anywhere you need to blow stuff 174 | up (delete code) 175 | - **Matthew the Muscle** 💪 will indicate that you're working with an exercise 176 | - **Chuck the Checkered Flag** 🏁 will indicate that you're working with a final 177 | - **Peter the Product Manager** 👨‍💼 helps us know what our users want 178 | - **Alfred the Alert** 🚨 will occasionally show up in the test failures with 179 | potential explanations for why the tests are failing. 180 | 181 | ## Contributors 182 | 183 | Thanks goes to these wonderful people 184 | ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 |
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
Justin Dorfman
Justin Dorfman

🔍
Ben Ilegbodu
Ben Ilegbodu

📖
Jonathan Belcher
Jonathan Belcher

📖
Richard Hefner
Richard Hefner

💻
Zac Jones
Zac Jones

📖
Ricardo Busquet
Ricardo Busquet

📖
Arpan Chattopadhyay
Arpan Chattopadhyay

📖
Marco Moretti
Marco Moretti

💻
cindy
cindy

📖
Aaron Reisman
Aaron Reisman

📖
Jacob M-G Evans
Jacob M-G Evans

👀
Jesse Hull
Jesse Hull

📖
Tomas Caraccia
Tomas Caraccia

📖
Vasilii Kovalev
Vasilii Kovalev

💻
Felix Geelhaar
Felix Geelhaar

📖
Apola Kipso
Apola Kipso

📖
dcgoodwin2112
dcgoodwin2112

🐛
Pritam Sangani
Pritam Sangani

💻
Ryan Hinerman
Ryan Hinerman

📖 💻
Marco
Marco

🐛
Peter Hozák
Peter Hozák

💻
Emmanouil Zoumpoulakis
Emmanouil Zoumpoulakis

📖
Navneet Sahota
Navneet Sahota

📖
Rodrigo Fuentes
Rodrigo Fuentes

📖
Johnny Magrippis
Johnny Magrippis

💻
Roshan Acharya
Roshan Acharya

📖
Art Telesh
Art Telesh

📖 💻
Amr A.Mohammed
Amr A.Mohammed

🤔
Douglas
Douglas

📖
Gaurav
Gaurav

💻
LauraOneasca
LauraOneasca

📖
Michaël De Boey
Michaël De Boey

💻
Hannes Diercks
Hannes Diercks

⚠️
Luke
Luke

🐛
Toni Dezman
Toni Dezman

💻
Bobby Warner
Bobby Warner

💻
Konrad Szałwiński
Konrad Szałwiński

💻
Yaseen Kadir
Yaseen Kadir

🐛
Kevin Viglucci
Kevin Viglucci

📖
Iliyan Trifonov
Iliyan Trifonov

📖
Oscar Dominguez
Oscar Dominguez

📖
Aaron Casanova
Aaron Casanova

💻
Kyle Gach
Kyle Gach

📖
Anthony Diké
Anthony Diké

📖
Matt Schlenker
Matt Schlenker

📖
Jakub
Jakub

📖
Victor Debone
Victor Debone

📖
Mahbub Zaman
Mahbub Zaman

📖
Misaki Okajima
Misaki Okajima

📖
Mario Sannum
Mario Sannum

💻
Katherine Peterson
Katherine Peterson

📖
Alex Watt
Alex Watt

📖
Ned Redmond
Ned Redmond

⚠️
Oz Adari
Oz Adari

📖
Chiranjeev Gupta
Chiranjeev Gupta

📖
Sunnatullo Ganiev
Sunnatullo Ganiev

📖
Joey Cardosi
Joey Cardosi

📖
Luca
Luca

📖
270 | 271 | 272 | 273 | 274 | 275 | 276 | This project follows the 277 | [all-contributors](https://github.com/kentcdodds/all-contributors) 278 | specification. Contributions of any kind welcome! 279 | 280 | ## Workshop Feedback 281 | 282 | Each exercise has an Elaboration and Feedback link. Please fill that out after 283 | the exercise and instruction. 284 | 285 | At the end of the workshop, please go to this URL to give overall feedback. 286 | Thank you! https://kcd.im/rf-ws-feedback 287 | 288 | 289 | [npm]: https://www.npmjs.com/ 290 | [node]: https://nodejs.org 291 | [git]: https://git-scm.com/ 292 | [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/react-fundamentals/validate/main?logo=github&style=flat-square 293 | [build]: https://github.com/kentcdodds/react-fundamentals/actions?query=workflow%3Avalidate 294 | [license-badge]: https://img.shields.io/badge/license-GPL%203.0%20License-blue.svg?style=flat-square 295 | [license]: https://github.com/kentcdodds/react-fundamentals/blob/main/LICENSE 296 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 297 | [gitpod-badge]: https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod 298 | [coc]: https://github.com/kentcdodds/react-fundamentals/blob/main/CODE_OF_CONDUCT.md 299 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 300 | [all-contributors]: https://github.com/kentcdodds/all-contributors 301 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/react-fundamentals?color=orange&style=flat-square 302 | [win-path]: https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/ 303 | [mac-path]: http://stackoverflow.com/a/24322978/971592 304 | [issue]: https://github.com/kentcdodds/react-fundamentals/issues/new 305 | 306 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@kentcdodds/react-workshop-app/craco.config') 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node: 5 | build: . 6 | volumes: 7 | - ./src:/app/src 8 | ports: 9 | - '3000:3000' 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kentcdodds/react-fundamentals", 3 | "title": "React Fundamentals ⚛", 4 | "description": "The material for learning React fundamentals", 5 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 6 | "version": "1.0.0", 7 | "private": true, 8 | "keywords": [], 9 | "homepage": "http://react-fundamentals.netlify.app/", 10 | "license": "GPL-3.0-only", 11 | "main": "src/index.js", 12 | "engines": { 13 | "node": "14 || 16 || 18", 14 | "npm": ">=8.16.0" 15 | }, 16 | "dependencies": { 17 | "@kentcdodds/react-workshop-app": "^6.0.1", 18 | "@testing-library/react": "^13.3.0", 19 | "@testing-library/user-event": "^14.2.1", 20 | "@types/react": "^18.0.14", 21 | "@types/react-dom": "^18.0.5", 22 | "chalk": "^4.1.2", 23 | "codegen.macro": "^4.1.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0" 26 | }, 27 | "devDependencies": { 28 | "@craco/craco": "^6.4.3", 29 | "husky": "^4.3.8", 30 | "npm-run-all": "^4.1.5", 31 | "prettier": "^2.7.1", 32 | "react-scripts": "^5.0.1", 33 | "typescript": "^4.7.4" 34 | }, 35 | "scripts": { 36 | "start": "craco start", 37 | "build": "craco build", 38 | "test": "craco test --env=jsdom", 39 | "test:coverage": "npm run test -- --watchAll=false", 40 | "test:exercises": "npm run test -- testing.*exercises\\/ --onlyChanged", 41 | "setup": "node setup", 42 | "lint": "eslint .", 43 | "format": "prettier --write \"./src\"", 44 | "validate": "npm-run-all --parallel build test:coverage lint" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "node ./scripts/pre-commit", 49 | "pre-push": "node ./scripts/pre-push" 50 | } 51 | }, 52 | "jest": { 53 | "collectCoverageFrom": [ 54 | "src/final/**/*.js" 55 | ] 56 | }, 57 | "eslintConfig": { 58 | "extends": "react-app" 59 | }, 60 | "browserslist": { 61 | "development": [ 62 | "last 2 chrome versions", 63 | "last 2 firefox versions", 64 | "last 2 edge versions" 65 | ], 66 | "production": [ 67 | ">1%", 68 | "last 4 versions", 69 | "Firefox ESR", 70 | "not ie < 11" 71 | ] 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/kentcdodds/react-fundamentals.git" 76 | }, 77 | "bugs": { 78 | "url": "https://github.com/kentcdodds/react-fundamentals/issues" 79 | }, 80 | "msw": { 81 | "workerDirectory": "public" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kangarruu/react-fundamentals/51d0273dd092a9462643e20221c41dff5ea58d1a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | React Fundamentals ⚛ 13 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Fundamentals", 3 | "name": "React Fundamentals ⚛", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#1675ff", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (client && activeClientIds.has(client.id)) { 118 | ;(async function () { 119 | const clonedResponse = response.clone() 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: 129 | clonedResponse.body === null ? null : await clonedResponse.text(), 130 | headers: serializeHeaders(clonedResponse.headers), 131 | redirected: clonedResponse.redirected, 132 | }, 133 | }) 134 | })() 135 | } 136 | 137 | return response 138 | } 139 | 140 | async function getResponse(event, client, requestId) { 141 | const { request } = event 142 | const requestClone = request.clone() 143 | const getOriginalResponse = () => fetch(requestClone) 144 | 145 | // Bypass mocking when the request client is not active. 146 | if (!client) { 147 | return getOriginalResponse() 148 | } 149 | 150 | // Bypass initial page load requests (i.e. static assets). 151 | // The absence of the immediate/parent client in the map of the active clients 152 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 153 | // and is not ready to handle requests. 154 | if (!activeClientIds.has(client.id)) { 155 | return await getOriginalResponse() 156 | } 157 | 158 | // Bypass requests with the explicit bypass header 159 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 160 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 161 | 162 | // Remove the bypass header to comply with the CORS preflight check. 163 | delete cleanRequestHeaders[bypassHeaderName] 164 | 165 | const originalRequest = new Request(requestClone, { 166 | headers: new Headers(cleanRequestHeaders), 167 | }) 168 | 169 | return fetch(originalRequest) 170 | } 171 | 172 | // Send the request to the client-side MSW. 173 | const reqHeaders = serializeHeaders(request.headers) 174 | const body = await request.text() 175 | 176 | const clientMessage = await sendToClient(client, { 177 | type: 'REQUEST', 178 | payload: { 179 | id: requestId, 180 | url: request.url, 181 | method: request.method, 182 | headers: reqHeaders, 183 | cache: request.cache, 184 | mode: request.mode, 185 | credentials: request.credentials, 186 | destination: request.destination, 187 | integrity: request.integrity, 188 | redirect: request.redirect, 189 | referrer: request.referrer, 190 | referrerPolicy: request.referrerPolicy, 191 | body, 192 | bodyUsed: request.bodyUsed, 193 | keepalive: request.keepalive, 194 | }, 195 | }) 196 | 197 | switch (clientMessage.type) { 198 | case 'MOCK_SUCCESS': { 199 | return delayPromise( 200 | () => respondWithMock(clientMessage), 201 | clientMessage.payload.delay, 202 | ) 203 | } 204 | 205 | case 'MOCK_NOT_FOUND': { 206 | return getOriginalResponse() 207 | } 208 | 209 | case 'NETWORK_ERROR': { 210 | const { name, message } = clientMessage.payload 211 | const networkError = new Error(message) 212 | networkError.name = name 213 | 214 | // Rejecting a request Promise emulates a network error. 215 | throw networkError 216 | } 217 | 218 | case 'INTERNAL_ERROR': { 219 | const parsedBody = JSON.parse(clientMessage.payload.body) 220 | 221 | console.error( 222 | `\ 223 | [MSW] Request handler function for "%s %s" has thrown the following exception: 224 | 225 | ${parsedBody.errorType}: ${parsedBody.message} 226 | (see more detailed error stack trace in the mocked response body) 227 | 228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 229 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 230 | `, 231 | request.method, 232 | request.url, 233 | ) 234 | 235 | return respondWithMock(clientMessage) 236 | } 237 | } 238 | 239 | return getOriginalResponse() 240 | } 241 | 242 | self.addEventListener('fetch', function (event) { 243 | const { request } = event 244 | 245 | // Bypass navigation requests. 246 | if (request.mode === 'navigate') { 247 | return 248 | } 249 | 250 | // Opening the DevTools triggers the "only-if-cached" request 251 | // that cannot be handled by the worker. Bypass such requests. 252 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 253 | return 254 | } 255 | 256 | // Bypass all requests when there are no active clients. 257 | // Prevents the self-unregistered worked from handling requests 258 | // after it's been deleted (still remains active until the next reload). 259 | if (activeClientIds.size === 0) { 260 | return 261 | } 262 | 263 | const requestId = uuidv4() 264 | 265 | return event.respondWith( 266 | handleRequest(event, requestId).catch((error) => { 267 | console.error( 268 | '[MSW] Failed to mock a "%s" request to "%s": %s', 269 | request.method, 270 | request.url, 271 | error, 272 | ) 273 | }), 274 | ) 275 | }) 276 | 277 | function serializeHeaders(headers) { 278 | const reqHeaders = {} 279 | headers.forEach((value, name) => { 280 | reqHeaders[name] = reqHeaders[name] 281 | ? [].concat(reqHeaders[name]).concat(value) 282 | : value 283 | }) 284 | return reqHeaders 285 | } 286 | 287 | function sendToClient(client, message) { 288 | return new Promise((resolve, reject) => { 289 | const channel = new MessageChannel() 290 | 291 | channel.port1.onmessage = (event) => { 292 | if (event.data && event.data.error) { 293 | return reject(event.data.error) 294 | } 295 | 296 | resolve(event.data) 297 | } 298 | 299 | client.postMessage(JSON.stringify(message), [channel.port2]) 300 | }) 301 | } 302 | 303 | function delayPromise(cb, duration) { 304 | return new Promise((resolve) => { 305 | setTimeout(() => resolve(cb()), duration) 306 | }) 307 | } 308 | 309 | function respondWithMock(clientMessage) { 310 | return new Response(clientMessage.payload.body, { 311 | ...clientMessage.payload, 312 | headers: clientMessage.payload.headers, 313 | }) 314 | } 315 | 316 | function uuidv4() { 317 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 318 | const r = (Math.random() * 16) | 0 319 | const v = c == 'x' ? r : (r & 0x3) | 0x8 320 | return v.toString(16) 321 | }) 322 | } 323 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "startScript": "start", 5 | "port": 3000, 6 | "node": "14" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/diff.js: -------------------------------------------------------------------------------- 1 | const {spawnSync} = require('child_process') 2 | const inquirer = require('inquirer') 3 | const glob = require('glob') 4 | 5 | async function go() { 6 | const files = glob 7 | .sync('src/+(exercise|final)/*.+(js|ts|tsx)', { 8 | ignore: ['*.d.ts'], 9 | }) 10 | .map(f => f.replace(/^src\//, '')) 11 | const {first} = await inquirer.prompt([ 12 | { 13 | name: 'first', 14 | message: `What's the first file`, 15 | type: 'list', 16 | choices: files, 17 | }, 18 | ]) 19 | const {second} = await inquirer.prompt([ 20 | { 21 | name: 'second', 22 | message: `What's the second file`, 23 | type: 'list', 24 | choices: files.filter(f => f !== first), 25 | }, 26 | ]) 27 | 28 | spawnSync(`git diff --no-index ./src/${first} ./src/${second}`, { 29 | shell: true, 30 | stdio: 'inherit', 31 | }) 32 | } 33 | 34 | go() 35 | -------------------------------------------------------------------------------- /scripts/fix-links: -------------------------------------------------------------------------------- 1 | npx https://gist.github.com/kentcdodds/436a77ff8977269e5fee39d9d89956de 2 | npm run format 3 | -------------------------------------------------------------------------------- /scripts/pre-commit.js: -------------------------------------------------------------------------------- 1 | var spawnSync = require('child_process').spawnSync 2 | const {username} = require('os').userInfo() 3 | 4 | if (username === 'kentcdodds') { 5 | const result = spawnSync('npm run validate', {stdio: 'inherit', shell: true}) 6 | 7 | if (result.status !== 0) { 8 | process.exit(result.status) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/pre-push.js: -------------------------------------------------------------------------------- 1 | try { 2 | const {username} = require('os').userInfo() 3 | const { 4 | repository: {url: repoUrl}, 5 | } = require('../package.json') 6 | 7 | const remote = process.env.HUSKY_GIT_PARAMS.split(' ')[1] 8 | const repoName = repoUrl.match(/(?:.(?!\/))+\.git$/)[0] 9 | if (username !== 'kentcdodds' && remote.includes(`kentcdodds${repoName}`)) { 10 | console.log( 11 | `You're trying to push to Kent's repo directly. If you want to save and push your work or even make a contribution to the workshop material, you'll need to fork the repo first and push changes to your fork. Learn how here: https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo`, 12 | ) 13 | process.exit(1) 14 | } 15 | } catch (error) { 16 | // ignore 17 | } 18 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | var spawnSync = require('child_process').spawnSync 2 | 3 | var styles = { 4 | // got these from playing around with what I found from: 5 | // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96 6 | // they're the best I could find that works well for light or dark terminals 7 | success: {open: '\u001b[32;1m', close: '\u001b[0m'}, 8 | danger: {open: '\u001b[31;1m', close: '\u001b[0m'}, 9 | info: {open: '\u001b[36;1m', close: '\u001b[0m'}, 10 | subtitle: {open: '\u001b[2;1m', close: '\u001b[0m'}, 11 | } 12 | 13 | function color(modifier, string) { 14 | return styles[modifier].open + string + styles[modifier].close 15 | } 16 | 17 | console.log(color('info', '▶️ Starting workshop setup...')) 18 | 19 | var output = spawnSync('npm --version', {shell: true}).stdout.toString().trim() 20 | var outputParts = output.split('.') 21 | var major = Number(outputParts[0]) 22 | var minor = Number(outputParts[1]) 23 | if (major < 8 || (major === 8 && minor < 16)) { 24 | console.error( 25 | color( 26 | 'danger', 27 | '🚨 npm version is ' + 28 | output + 29 | ' which is out of date. Please install npm@8.16.0 or greater', 30 | ), 31 | ) 32 | throw new Error('npm version is out of date') 33 | } 34 | 35 | var command = 36 | 'npx "https://gist.github.com/kentcdodds/bb452ffe53a5caa3600197e1d8005733" -q' 37 | console.log( 38 | color('subtitle', ' Running the following command: ' + command), 39 | ) 40 | 41 | var result = spawnSync(command, {stdio: 'inherit', shell: true}) 42 | 43 | if (result.status === 0) { 44 | console.log(color('success', '✅ Workshop setup complete...')) 45 | } else { 46 | process.exit(result.status) 47 | } 48 | 49 | /* 50 | eslint 51 | no-var: "off", 52 | "vars-on-top": "off", 53 | */ 54 | -------------------------------------------------------------------------------- /scripts/update-deps: -------------------------------------------------------------------------------- 1 | # prettier-ignore 2 | npx npm-check-updates --upgrade --reject husky,chalk 3 | rm -rf node_modules package-lock.json 4 | npx npm@8 install 5 | npm run validate 6 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | require('./scripts/setup') 2 | 3 | -------------------------------------------------------------------------------- /src/__tests__/05.js: -------------------------------------------------------------------------------- 1 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 2 | import chalk from 'chalk' 3 | import {render, screen, prettyDOM} from '@testing-library/react' 4 | import App from '../final/05' 5 | // import App from '../exercise/05' 6 | 7 | test('renders the correct styles new', async () => { 8 | render() 9 | const allBoxes = screen.getAllByText(/box/i) 10 | 11 | const className = 'box' 12 | allBoxes.forEach(box => { 13 | alfredTip( 14 | () => { 15 | expect(box).toHaveClass(className) 16 | }, 17 | () => 18 | ` 19 | This box is missing the className "${className}" 20 | 21 | ${chalk.reset(prettyDOM(box))} 22 | `.trim(), 23 | ) 24 | }) 25 | 26 | allBoxes.forEach(box => { 27 | alfredTip( 28 | () => { 29 | expect(box).toHaveStyle('font-style: italic;') 30 | }, 31 | () => 32 | ` 33 | This box is missing fontStyle: 'italic' in the style prop 34 | 35 | ${chalk.reset(prettyDOM(box))} 36 | `.trim(), 37 | ) 38 | }) 39 | 40 | const small = screen.getByText(/small/i) 41 | const medium = screen.getByText(/medium/i) 42 | const large = screen.getByText(/large/i) 43 | 44 | expect(small).toHaveClass('box--small') 45 | expect(small).toHaveStyle('background-color: lightblue;') 46 | 47 | expect(medium).toHaveClass('box--medium') 48 | expect(medium).toHaveStyle('background-color: pink;') 49 | 50 | expect(large).toHaveClass('box--large') 51 | expect(large).toHaveStyle('background-color: orange;') 52 | }) 53 | -------------------------------------------------------------------------------- /src/__tests__/06.extra-2.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {alfredTip} from '@kentcdodds/react-workshop-app/test-utils' 3 | import {render, screen} from '@testing-library/react' 4 | import userEvent from '@testing-library/user-event' 5 | import App from '../final/06.extra-2' 6 | // import App from '../exercise/06' 7 | 8 | beforeAll(() => { 9 | jest.spyOn(global, 'alert').mockImplementation(() => {}) 10 | }) 11 | 12 | beforeEach(() => { 13 | global.alert.mockClear() 14 | }) 15 | 16 | test('calls the onSubmitUsername handler when the submit is fired', async () => { 17 | render() 18 | const input = screen.getByLabelText(/username/i) 19 | const submit = screen.getByText(/submit/i) 20 | 21 | let value = 'A' 22 | await userEvent.type(input, value) 23 | expect(submit).toBeDisabled() // upper-case 24 | 25 | const output = screen.getByText(/lower\s?case/i) 26 | expect(output).toBeInTheDocument() 27 | alfredTip( 28 | output.getAttribute('role') !== 'alert', 29 | 'Add an attribute `role="alert"` to the div to help with screen reader users.', 30 | ) 31 | await userEvent.clear(input) 32 | value = 'a' 33 | await userEvent.type(input, value) 34 | await userEvent.click(submit) 35 | 36 | expect(global.alert).toHaveBeenCalledWith(`You entered: ${input.value}`) 37 | expect(global.alert).toHaveBeenCalledTimes(1) 38 | }) 39 | -------------------------------------------------------------------------------- /src/__tests__/06.extra-3.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen} from '@testing-library/react' 3 | import userEvent from '@testing-library/user-event' 4 | import App from '../final/06.extra-3' 5 | // import App from '../exercise/06' 6 | 7 | beforeAll(() => { 8 | jest.spyOn(global, 'alert').mockImplementation(() => {}) 9 | }) 10 | 11 | beforeEach(() => { 12 | global.alert.mockClear() 13 | }) 14 | 15 | test('calls the onSubmitUsername handler when the submit is fired', async () => { 16 | render() 17 | const input = screen.getByLabelText(/username/i) 18 | const submit = screen.getByText(/submit/i) 19 | 20 | const value = 'A' 21 | await userEvent.type(input, value) 22 | expect(input.value).toBe('a') 23 | await userEvent.click(submit) 24 | 25 | expect(global.alert).toHaveBeenCalledWith(`You entered: ${input.value}`) 26 | expect(global.alert).toHaveBeenCalledTimes(1) 27 | }) 28 | -------------------------------------------------------------------------------- /src/__tests__/06.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen} from '@testing-library/react' 3 | import userEvent from '@testing-library/user-event' 4 | import App from '../final/06' 5 | // import App from '../exercise/06' 6 | 7 | beforeAll(() => { 8 | jest.spyOn(global, 'alert').mockImplementation(() => {}) 9 | }) 10 | 11 | beforeEach(() => { 12 | global.alert.mockClear() 13 | }) 14 | 15 | test('calls the onSubmitUsername handler when the submit is fired', async () => { 16 | render() 17 | const input = screen.getByLabelText(/username/i) 18 | const submit = screen.getByText(/submit/i) 19 | 20 | const username = 'jenny' 21 | 22 | await userEvent.type(input, username) 23 | await userEvent.click(submit) 24 | 25 | expect(global.alert).toHaveBeenCalledWith(`You entered: ${username}`) 26 | expect(global.alert).toHaveBeenCalledTimes(1) 27 | }) 28 | -------------------------------------------------------------------------------- /src/__tests__/07.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen, within} from '@testing-library/react' 3 | import userEvent from '@testing-library/user-event' 4 | import App from '../final/07' 5 | // import App from '../exercise/07' 6 | 7 | test('renders', async () => { 8 | const {container} = render() 9 | const plus = screen.getByText(/add item/i) 10 | await userEvent.click(plus) 11 | await userEvent.click(plus) 12 | await userEvent.click(plus) 13 | await userEvent.click(plus) 14 | 15 | const orangeInput = screen.getByLabelText(/orange/i) 16 | const orangeContainer = screen.getByText(/orange/i).closest('li') 17 | const inOrange = within(orangeContainer) 18 | await userEvent.type(orangeInput, 'sup dawg') 19 | await userEvent.click(inOrange.getByText('remove')) 20 | 21 | const allLis = container.querySelectorAll('li') 22 | Array.from(allLis).forEach(li => { 23 | const label = li.querySelector('label') 24 | const input = li.querySelector('input') 25 | expect(label.textContent).toBe(input.value) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/box-styles.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid #333; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | .box--large { 9 | width: 270px; 10 | height: 270px; 11 | } 12 | .box--medium { 13 | width: 180px; 14 | height: 180px; 15 | } 16 | .box--small { 17 | width: 90px; 18 | height: 90px; 19 | } 20 | -------------------------------------------------------------------------------- /src/exercise/01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/exercise/01.md: -------------------------------------------------------------------------------- 1 | # Basic JavaScript-rendered Hello World 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/01.md` 6 | 7 | ## Background 8 | 9 | It doesn't take long to learn how to make "Hello World" appear on the page with 10 | HTML: 11 | 12 | ```html 13 | 14 | 15 |
Hello World
16 | 17 | 18 | ``` 19 | 20 | The browser takes this HTML code and generates 21 | [the DOM (the Document Object Model)](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) 22 | out of it. The browser then exposes the DOM to JavaScript so you can interact 23 | with it to add a layer of interactivity to your web-page. 24 | 25 | ```html 26 | 27 | 28 |
Hello World
29 | 32 | 33 | 34 | ``` 35 | 36 | Years ago, people were generating HTML on the server and then adding JavaScript 37 | on top of that generated HTML for interactivity. However, as requirements for 38 | that interactivity became more challenging, this approach produced applications 39 | that were difficult to maintain and had performance issues. 40 | 41 | So modern JavaScript frameworks were created to address some of the challenges 42 | by programmatically creating the DOM rather than defining it in hand-written 43 | HTML. 44 | 45 | ## Exercise 46 | 47 | Production deploys: 48 | 49 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/01.html) 50 | - [Final](http://react-fundamentals.netlify.app/isolated/final/01.html) 51 | 52 | It's important to have a basic understanding of how to generate and interact 53 | with DOM nodes using JavaScript because it will help you understand how React 54 | works under the hood a little better. So in this exercise we're actually not 55 | going to use React at all. Instead we're going to use JavaScript to create a 56 | `div` DOM node with the text "Hello World" and insert that DOM node into the 57 | document. 58 | 59 | ## Extra Credit 60 | 61 | ### 1. 💯 generate the root node 62 | 63 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/01.extra-1.html) 64 | 65 | Rather than having the `root` node in the HTML, see if you can create that one 66 | using JavaScript as well. 67 | 68 | ## 🦉 Feedback 69 | 70 | Fill out 71 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=01%3A%20Basic%20JavaScript-rendered%20Hello%20World&em=). 72 | -------------------------------------------------------------------------------- /src/exercise/02.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 12 | 13 | 29 | 30 | -------------------------------------------------------------------------------- /src/exercise/02.md: -------------------------------------------------------------------------------- 1 | # Intro to raw React APIs 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/02.md` 6 | 7 | ## Background 8 | 9 | React is the most widely used frontend framework in the world and it's using the 10 | same APIs that you're using when it creates DOM nodes. 11 | 12 | > In fact, 13 | > [here's where that happens in the React source code](https://github.com/facebook/react/blob/48907797294340b6d5d8fecfbcf97edf0691888d/packages/react-dom/src/client/ReactDOMComponent.js#L416) 14 | > at the time of this writing. 15 | 16 | React abstracts away the imperative browser API from you to give you a much more 17 | declarative API to work with. 18 | 19 | > Learn more about the difference between those two concepts here: 20 | > [Imperative vs Declarative Programming](https://tylermcginnis.com/imperative-vs-declarative-programming/) 21 | 22 | One important thing to know about React is that it supports multiple platforms 23 | (for example, native and web). Each of these platforms has its own code 24 | necessary for interacting with that platform, and then there's shared code 25 | between the platforms. 26 | 27 | With that in mind, you need two JavaScript files to write React applications for 28 | the web: 29 | 30 | - React: responsible for creating React elements (kinda like 31 | `document.createElement()`) 32 | - ReactDOM: responsible for rendering React elements to the DOM (kinda like 33 | `rootElement.append()`) 34 | 35 | ## Exercise 36 | 37 | Production deploys: 38 | 39 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/02.html) 40 | - [Final](http://react-fundamentals.netlify.app/isolated/final/02.html) 41 | 42 | Let's convert this to use React! But don't worry, we won't be doing any JSX just 43 | yet... You're going to use raw React APIs here. 44 | 45 | In modern applications you'll get React and React DOM files from a "package 46 | registry" like [npm](https://npmjs.com) ([react](https://npm.im/react) and 47 | [react-dom](https://npm.im/react-dom)). But for these first exercises, we'll use 48 | the script files which are available on [unpkg.com](https://unpkg.com) and 49 | regular script tags so you don't have to bother installing them. So in the 50 | exercise you'll be required to add script tags for these files. 51 | 52 | Once you include the script tags, you'll have two new global variables to use: 53 | `React` and `ReactDOM`. 54 | 55 | Here's a simple example of the API: 56 | 57 | ```javascript 58 | const elementProps = {id: 'element-id', children: 'Hello world!'} 59 | const elementType = 'h1' 60 | const reactElement = React.createElement(elementType, elementProps) 61 | const root = ReactDOM.createRoot(rootElement) 62 | root.render(reactElement) 63 | ``` 64 | 65 | > 🦉 NOTE: prior to React v18, the API was: `ReactDOM.render` and that's what 66 | > you'll see in the EpicReact.dev videos. This material has been updated, so 67 | > you'll want to use the new `ReactDOM.createRoot` API as demonstrated above. 68 | 69 | Alright! Let's do this! 70 | 71 | ## Extra Credit 72 | 73 | ### 1. 💯 nesting elements 74 | 75 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/02.extra-1.html) 76 | 77 | See if you can figure out how to write the JavaScript + React code to generate 78 | this DOM output: 79 | 80 | ```html 81 | 82 |
83 |
84 | Hello 85 | World 86 |
87 |
88 | 89 | ``` 90 | 91 | ## 🦉 Feedback 92 | 93 | Fill out 94 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=02%3A%20Intro%20to%20raw%20React%20APIs&em=). 95 | -------------------------------------------------------------------------------- /src/exercise/03.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 13 | 14 | 32 | 33 | -------------------------------------------------------------------------------- /src/exercise/03.md: -------------------------------------------------------------------------------- 1 | # Using JSX 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/03.md` 6 | 7 | ## Background 8 | 9 | JSX is more intuitive than the raw React API and is easier to understand when 10 | reading the code. It's fairly simple HTML-like syntactic sugar on top of the raw 11 | React APIs: 12 | 13 | ```jsx 14 | const ui =

Hey there

15 | 16 | // ↓ ↓ ↓ ↓ compiles to ↓ ↓ ↓ ↓ 17 | 18 | const ui = React.createElement('h1', {id: 'greeting', children: 'Hey there'}) 19 | ``` 20 | 21 | Because JSX is not actually JavaScript, you have to convert it using something 22 | called a code compiler. [Babel](https://babeljs.io) is one such tool. 23 | 24 | 🦉 Pro tip: If you'd like to see how JSX gets compiled to JavaScript, 25 | [check out the online babel REPL here](https://babeljs.io/repl#?builtIns=App&code_lz=MYewdgzgLgBArgSxgXhgHgCYIG4D40QAOAhmLgBICmANtSGgPRGm7rNkDqIATtRo-3wMseAFBA&presets=react&prettier=true). 26 | 27 | If you can train your brain to look at JSX and see the compiled version of that 28 | code, you'll be MUCH more effective at reading and using it! I strongly 29 | recommend you give this some intentional practice. 30 | 31 | ## Exercise 32 | 33 | Production deploys: 34 | 35 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/03.html) 36 | - [Final](http://react-fundamentals.netlify.app/isolated/final/03.html) 37 | 38 | Normally you'll compile all of your code at build-time before you ship your 39 | application to the browser, but because Babel is written in JavaScript we can 40 | actually run it _in_ the browser to compile our code on the fly and that's what 41 | we'll do in this exercise. 42 | 43 | So we'll include a script tag for Babel, then we'll update our own script tag to 44 | tell Babel to compile it for us on the fly. When you're done, you should notice 45 | the compiled version of the code appears in the `` of the DOM (which you 46 | can inspect using DevTools). 47 | 48 | ## Extra Credit 49 | 50 | ### 1. 💯 interpolate className and children 51 | 52 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/03.extra-1.html) 53 | 54 | "Interpolation" is defined as "the insertion of something of a different nature 55 | into something else." 56 | 57 | Let's take template literals for example: 58 | 59 | ```javascript 60 | const greeting = 'Sup' 61 | const subject = 'World' 62 | const message = `${greeting} ${subject}` 63 | ``` 64 | 65 | See if you can figure out how to extract the `className` (`"container"`) and 66 | `children` (`"Hello World"`) to variables and interpolate them in the JSX. 67 | 68 | ```jsx 69 | const className = 'container' 70 | const children = 'Hello World' 71 | const element =
how do I make this work?
72 | ``` 73 | 74 | 📜 The react docs for JSX are pretty good: 75 | [https://reactjs.org/docs/introducing-jsx.html](https://reactjs.org/docs/introducing-jsx.html) 76 | 77 | Here are a few sections of particular interest for this extra credit: 78 | 79 | - [https://reactjs.org/docs/introducing-jsx.html#embedding-expressions-in-jsx](https://reactjs.org/docs/introducing-jsx.html#embedding-expressions-in-jsx) 80 | - [https://reactjs.org/docs/introducing-jsx.html#specifying-attributes-with-jsx](https://reactjs.org/docs/introducing-jsx.html#specifying-attributes-with-jsx) 81 | 82 | ### 2. 💯 spread props 83 | 84 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/03.extra-2.html) 85 | 86 | What if I have an object of props that I want applied to the `div` like this: 87 | 88 | ```jsx 89 | const children = 'Hello World' 90 | const className = 'container' 91 | const props = {children, className} 92 | const element =
// how do I apply props to this div? 93 | ``` 94 | 95 | If we were doing raw React APIs it would be: 96 | 97 | ```jsx 98 | const element = React.createElement('div', props) 99 | ``` 100 | 101 | Or, it could be written like this: 102 | 103 | ```jsx 104 | const element = React.createElement('div', {...props}) 105 | ``` 106 | 107 | See if you can figure out how to make that work with JSX. 108 | 109 | 📜 [https://reactjs.org/docs/jsx-in-depth.html#spread-attributes](https://reactjs.org/docs/jsx-in-depth.html#spread-attributes) 110 | 111 | ## 🦉 Feedback 112 | 113 | Fill out 114 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=03%3A%20Using%20JSX&em=). 115 | -------------------------------------------------------------------------------- /src/exercise/04.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /src/exercise/04.md: -------------------------------------------------------------------------------- 1 | # Creating custom components 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/04.md` 6 | 7 | ## Background 8 | 9 | Just like in regular JavaScript, you often want to share code which you do using 10 | functions. If you want to share JSX, you can do that as well. In React we call 11 | these functions "components" and they have some special properties. 12 | 13 | Components are basically functions which return something that is "renderable" 14 | (more React elements, strings, `null`, numbers, etc.) 15 | 16 | ## Exercise 17 | 18 | Production deploys: 19 | 20 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/04.html) 21 | - [Final](http://react-fundamentals.netlify.app/isolated/final/04.html) 22 | 23 | Let's say the DOM we want to generate is like this: 24 | 25 | ```html 26 |
27 |
Hello World
28 |
Goodbye World
29 |
30 | ``` 31 | 32 | In this case, it would be cool if we could reduce the duplication for creating 33 | the React elements for this: 34 | 35 | ```jsx 36 |
{children}
37 | ``` 38 | 39 | So we need to make a function which accepts an object argument with a `children` 40 | property and returns the React element. Then you can interpolate a call to that 41 | function in your JSX. 42 | 43 | ```jsx 44 |
{message({children: 'Hello World'})}
45 | ``` 46 | 47 | This is not how we write custom React components, but this is important for you 48 | to understand them. We'll get to custom components in the extra credit. 49 | 50 | 📜 Read more 51 | 52 | - [https://reactjs.org/docs/jsx-in-depth.html](https://reactjs.org/docs/jsx-in-depth.html) 53 | - [https://kentcdodds.com/blog/what-is-jsx](https://kentcdodds.com/blog/what-is-jsx) 54 | 55 | ## Extra Credit 56 | 57 | ### 1. 💯 using a custom component with React.createElement 58 | 59 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-1.html) 60 | 61 | So far we've only used `React.createElement(someString)`, but the first argument 62 | to `React.createElement` can also be a function which returns something that's 63 | renderable. 64 | 65 | So instead of calling your `message` function, pass it as the first argument to 66 | `React.createElement` and pass the `{children: 'Hello World'}` object as the 67 | second argument. 68 | 69 | ### 2. 💯 using a custom component with JSX 70 | 71 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-2.html) 72 | 73 | We're so close! Just like using JSX for regular `div`s is nicer than using the 74 | raw `React.createElement` API, using JSX for custom components is nicer too. 75 | Remember that it's Babel that's responsible for taking our JSX and compiling it 76 | to `React.createElement` calls so we just need a way to tell Babel how to 77 | compile our JSX so it passes the function by its name rather than a string. 78 | 79 | We do this by how the JSX appears. Here are a few examples of Babel output for 80 | JSX: 81 | 82 | ```javascript 83 | ui = // React.createElement(Capitalized) 84 | ui = // React.createElement(property.access) 85 | ui = // React.createElement(Property.Access) 86 | ui = // SyntaxError 87 | ui = // React.createElement('lowercase') 88 | ui = // React.createElement('kebab-case') 89 | ui = // React.createElement('Upper-Kebab-Case') 90 | ui = // React.createElement(Upper_Snake_Case) 91 | ui = // React.createElement('lower_snake_case') 92 | ``` 93 | 94 | See if you can change your component function name so people can use it with JSX 95 | more easily! 96 | 97 | ### 3. 💯 Runtime validation with PropTypes 98 | 99 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-3.html) 100 | 101 | Let's change the Message component a little bit. Make it look like this now: 102 | 103 | ```javascript 104 | function Message({subject, greeting}) { 105 | return ( 106 |
107 | {greeting}, {subject} 108 |
109 | ) 110 | } 111 | ``` 112 | 113 | So now we'll use it like this: 114 | 115 | ```javascript 116 | 117 | 118 | ``` 119 | 120 | What happens if I forget to pass the `greeting` or `subject` props? It's not 121 | going to render properly. We'll end up with a dangling comma somewhere. It would 122 | be nice if we got some sort of indication that we passed the wrong value to the 123 | component. This is what the `propTypes` feature is for. Here's an example of how 124 | you use `propTypes`: 125 | 126 | ```javascript 127 | function FavoriteNumber({favoriteNumber}) { 128 | return
My favorite number is: {favoriteNumber}
129 | } 130 | 131 | const PropTypes = { 132 | number(props, propName, componentName) { 133 | if (typeof props[propName] !== 'number') { 134 | return new Error('Some useful error message here') 135 | } 136 | }, 137 | } 138 | 139 | FavoriteNumber.propTypes = { 140 | favoriteNumber: PropTypes.number, 141 | } 142 | ``` 143 | 144 | With that, if I do this: 145 | 146 | ```javascript 147 | 148 | ``` 149 | 150 | I'll get an error in the console. 151 | 152 | For this extra credit, add `propTypes` support to your updated component 153 | (remember to update it to have the subject and greeting). 154 | 155 | 🦉 Note that prop types validation add some runtime overhead resulting in sub-optimal 156 | performance, so the validation functions are not run in production. 157 | 158 | 📜 Read more about prop-types: 159 | 160 | - [https://reactjs.org/docs/typechecking-with-proptypes.html](https://reactjs.org/docs/typechecking-with-proptypes.html) 161 | 162 | ### 4. 💯 Use the prop-types package 163 | 164 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-4.html) 165 | 166 | As it turns out, there are some pretty common things you'd want to validate, so 167 | the React team maintains a package of these called 168 | [`prop-types`](https://npm.im/prop-types). Go ahead and get that added to the 169 | page by adding a script tag for it: 170 | 171 | ```html 172 | 173 | ``` 174 | 175 | Then use that package instead of writing it yourself. Also, make use of the 176 | `isRequired` feature! 177 | 178 | ### 5. 💯 using React Fragments 179 | 180 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/04.extra-5.html) 181 | 182 | One feature of JSX that you'll find useful is called 183 | ["React Fragments"](https://reactjs.org/docs/fragments.html). It's a special 184 | kind of component from React which allows you to position two elements 185 | side-by-side rather than just nested. 186 | 187 | The component is available via `` (or a 188 | [short syntax](https://reactjs.org/docs/fragments.html#short-syntax) that opens 189 | with `<>` and closes with ``). Replace the `
` with 190 | a fragment and inspect the DOM to notice that the elements are both rendered as 191 | direct children of `root`. 192 | 193 | ## 🦉 Feedback 194 | 195 | Fill out 196 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=04%3A%20Creating%20custom%20components&em=). 197 | -------------------------------------------------------------------------------- /src/exercise/05.js: -------------------------------------------------------------------------------- 1 | // Styling 2 | // http://localhost:3000/isolated/exercise/05.js 3 | 4 | import * as React from 'react' 5 | import '../box-styles.css' 6 | 7 | // 🐨 add a className prop to each div and apply the correct class names 8 | // based on the text content 9 | // 💰 Here are the available class names: box, box--large, box--medium, box--small 10 | // 💰 each of the elements should have the "box" className applied 11 | 12 | // 🐨 add a style prop to each div so their background color 13 | // matches what the text says it should be 14 | // 🐨 also use the style prop to make the font italic 15 | // 💰 Here are available style attributes: backgroundColor, fontStyle 16 | 17 | const smallBox =
small lightblue box
18 | const mediumBox =
medium pink box
19 | const largeBox =
large orange box
20 | 21 | function App() { 22 | return ( 23 |
24 | {smallBox} 25 | {mediumBox} 26 | {largeBox} 27 |
28 | ) 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /src/exercise/05.md: -------------------------------------------------------------------------------- 1 | # Styling 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/05.md` 6 | 7 | ## Background 8 | 9 | There are two primary ways to style react components 10 | 11 | 1. Inline styles with the `style` prop 12 | 2. Regular CSS with the `className` prop 13 | 14 | **About the `style` prop:** 15 | 16 | - In HTML you'd pass a string of CSS: 17 | 18 | ```html 19 |
20 | ``` 21 | 22 | - In React, you'll pass an object of CSS: 23 | 24 | ```jsx 25 |
26 | ``` 27 | 28 | Note that in react the `{{` and `}}` is actually a combination of a JSX 29 | expression and an object expression. The same example above could be written 30 | like so: 31 | 32 | ```jsx 33 | const myStyles = {marginTop: 20, backgroundColor: 'blue'} 34 |
35 | ``` 36 | 37 | Note also that the property names are `camelCased` rather than `kebab-cased`. 38 | This matches the `style` property of DOM nodes (which is a 39 | [`CSSStyleDeclaration`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) 40 | object). 41 | 42 | **About the `className` prop:** 43 | 44 | As we discussed earlier, in HTML, you apply a class name to an element with the 45 | `class` attribute. In JSX, you use the `className` prop. 46 | 47 | ## Exercise 48 | 49 | Production deploys: 50 | 51 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/05.js) 52 | - [Final](http://react-fundamentals.netlify.app/isolated/final/05.js) 53 | 54 | In this exercise we'll use both methods for styling react components. 55 | 56 | We have the following css on the page: 57 | 58 | ```css 59 | .box { 60 | border: 1px solid #333; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: center; 64 | text-align: center; 65 | } 66 | .box--large { 67 | width: 270px; 68 | height: 270px; 69 | } 70 | .box--medium { 71 | width: 180px; 72 | height: 180px; 73 | } 74 | .box--small { 75 | width: 90px; 76 | height: 90px; 77 | } 78 | ``` 79 | 80 | Your job is to apply the right className and style props to the divs so the 81 | styles applied match the text content. 82 | 83 | ## Extra Credit 84 | 85 | ### 1. 💯 Create a custom component 86 | 87 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-1.js) 88 | 89 | Try to make a custom `` component that renders a div, accepts all the 90 | props and merges the given `style` and `className` props with the shared values. 91 | 92 | I should be able to use it like so: 93 | 94 | ```jsx 95 | 96 | small lightblue box 97 | 98 | ``` 99 | 100 | The `box` className and `fontStyle: 'italic'` style should be applied in 101 | addition to the values that come from props. 102 | 103 | ### 2. 💯 accept a size prop to encapsulate styling 104 | 105 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/05.extra-2.js) 106 | 107 | It's great that we're composing the `className`s and `style`s properly, but 108 | wouldn't it be better if the users of our components didn't have to worry about 109 | which class name to apply for a given effect? Or that a class name is involved 110 | at all? I think it would be better if users of our component had a `size` prop 111 | and our component took care of making the box that size. 112 | 113 | In this extra credit, try to make this API work: 114 | 115 | ```jsx 116 | 117 | small lightblue box 118 | 119 | ``` 120 | 121 | ## Attribution 122 | 123 | [Matt Zabriskie](https://twitter.com/mzabriskie) developed this example 124 | originally for 125 | [a workshop we gave together.](https://github.com/mzabriskie/react-workshop) 126 | 127 | ## 🦉 Feedback 128 | 129 | Fill out 130 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=05%3A%20Styling&em=). 131 | -------------------------------------------------------------------------------- /src/exercise/06.js: -------------------------------------------------------------------------------- 1 | // Basic Forms 2 | // http://localhost:3000/isolated/exercise/06.js 3 | 4 | import * as React from 'react' 5 | 6 | function UsernameForm({onSubmitUsername}) { 7 | // 🐨 add a submit event handler here (`handleSubmit`). 8 | // 💰 Make sure to accept the `event` as an argument and call 9 | // `event.preventDefault()` to prevent the default behavior of form submit 10 | // events (which refreshes the page). 11 | // 📜 https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault 12 | // 13 | // 🐨 get the value from the username input (using whichever method 14 | // you prefer from the options mentioned in the instructions) 15 | // 💰 For example: event.target.elements[0].value 16 | // 🐨 Call `onSubmitUsername` with the value of the input 17 | 18 | // 🐨 add the onSubmit handler to the
below 19 | 20 | // 🐨 make sure to associate the label to the input. 21 | // to do so, set the value of 'htmlFor' prop of the label to the id of input 22 | return ( 23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | ) 31 | } 32 | 33 | function App() { 34 | const onSubmitUsername = username => alert(`You entered: ${username}`) 35 | return 36 | } 37 | 38 | export default App 39 | -------------------------------------------------------------------------------- /src/exercise/06.md: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/06.md` 6 | 7 | ## Background 8 | 9 | In React, there actually aren't a ton of things you have to learn to interact 10 | with forms beyond what you can do with regular DOM APIs and JavaScript. Which I 11 | think is pretty awesome. 12 | 13 | You can attach a submit handler to a form element with the `onSubmit` prop. This 14 | will be called with the submit event which has a `target`. That `target` is a 15 | reference to the `
` DOM node which has a reference to the elements of the 16 | form which can be used to get the values out of the form! 17 | 18 | ## Exercise 19 | 20 | Production deploys: 21 | 22 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/06.js) 23 | - [Final](http://react-fundamentals.netlify.app/isolated/final/06.js) 24 | 25 | In this exercise, we have a form where you can submit a username and then you'll 26 | get an "alert" showing what you typed. 27 | 28 | 🦉 There are several ways to get the value of the name input: 29 | 30 | - Via their index: `event.target.elements[0].value` 31 | - Via the elements object by their `name` or `id` attribute: 32 | `event.target.elements.usernameInput.value` 33 | - There's another that I'll save for the extra credit 34 | 35 | ## Extra Credit 36 | 37 | ### 1. 💯 using refs 38 | 39 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-1.js) 40 | 41 | Another way to get the value is via a `ref` in React. A `ref` is an object that 42 | stays consistent between renders of your React component. It has a `current` 43 | property on it which can be updated to any value at any time. In the case of 44 | interacting with DOM nodes, you can pass a `ref` to a React element and React 45 | will set the `current` property to the DOM node that's rendered. 46 | 47 | So if you create an `inputRef` object via `React.useRef`, you could access the 48 | value via: `inputRef.current.value` 49 | (📜[https://reactjs.org/docs/hooks-reference.html#useref](https://reactjs.org/docs/hooks-reference.html#useref)) 50 | 51 | Try to get the usernameInput's value using a ref. 52 | 53 | ### 2. 💯 Validate lower-case 54 | 55 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-2.js) 56 | 57 | With React, the way you use state is via a special "hook" called `useState`. 58 | Here's a simple example of what that looks like: 59 | 60 | ```jsx 61 | function Counter() { 62 | const [count, setCount] = React.useState(0) 63 | const increment = () => setCount(count + 1) 64 | return 65 | } 66 | ``` 67 | 68 | `React.useState` accepts a default initial value and returns an array. Typically 69 | you'll destructure that array to get the state and a state updater function. 70 | 71 | 📜 [https://reactjs.org/docs/hooks-state.html](https://reactjs.org/docs/hooks-state.html) 72 | 73 | In this extra credit, we're going to say that this username input only accepts 74 | lower-case characters. So if someone types an upper-case character, that's 75 | invalid input and we'll show an error message. 76 | 77 | If we want our form to be dynamic, we'll need a few things: 78 | 79 | 1. Component state to store the dynamic values (an error message in our case) 80 | 2. A change handler on the input so we know what the value is as the user 81 | changes it and can update the error state. 82 | 83 | Once we have that wired up then we can render the error message and disable the 84 | submit button if there's an error. 85 | 86 | 💰 This one's a little more tricky, so here are a few things you need to do to 87 | make this work: 88 | 89 | 1. Create a `handleChange` function that accepts the change `event` and uses 90 | `event.target.value` to get the value of the input. Remember this event will 91 | be triggered on the input, not the form. 92 | 2. Use the value of the input to determine whether there's an error. There's an 93 | error if the user typed any upper-case characters. You can check this really 94 | easily via `const isValid = value === value.toLowerCase()` 95 | 3. If there's an error, set the error state to `'Username must be lower case'`. 96 | (💰 here's how you do that: 97 | `setError(isValid ? null : 'Username must be lower case')`) and disable the 98 | submit button. 99 | 4. Finally, display the error in an element 100 | 101 | You may consider adding a `role="alert"` to the element you use to display the 102 | error to assist with screen reader users. 103 | 104 | Make sure you pass `handleChange` to the `onChange` handler of the `input`. 105 | 106 | ### 3. 💯 Control the input value 107 | 108 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/06.extra-3.js) 109 | 110 | Sometimes you have form inputs which you want to programmatically control. Maybe 111 | you want to set their value explicitly when the user clicks a button, or maybe 112 | you want to change what the value is as the user is typing. 113 | 114 | This is why React supports Controlled Form inputs. So far in our exercises, all 115 | of the form inputs have been "uncontrolled" which means that the browser is 116 | maintaining the state of the input by itself and we can be notified of changes 117 | and "query" for the value from the DOM node. 118 | 119 | If we want to explicitly update that value we could do this: 120 | `inputNode.value = 'whatever'` but that's pretty imperative. Instead, React 121 | allows us to programmatically set the `value` prop on the input like so: 122 | 123 | ```jsx 124 | 125 | ``` 126 | 127 | Once we do that, React ensures that the value of that input can never differ 128 | from the value of the `myInputValue` variable. 129 | 130 | Typically you'll want to provide an `onChange` handler as well so you can be 131 | made aware of "suggested changes" to the input's value (where React is basically 132 | saying "if I were controlling this value, here's what I would do, but you do 133 | whatever you want with this"). 134 | 135 | Typically you'll want to store the input's value in a state variable (via 136 | `React.useState`) and then the `onChange` handler will call the state updater to 137 | keep that value up-to-date. 138 | 139 | Wouldn't it be even cooler if instead of showing an error message we just didn't 140 | allow the user to enter invalid input? Yeah! In this extra credit I've backed us up 141 | and removed the error stuff and now we're going to control the input state and 142 | control the input value. Anytime there's a change we'll call `.toLowerCase()` on 143 | the value to ensure that it's always the lower case version of what the user 144 | types. 145 | 146 | So we can get rid of our `error` state and instead we'll manage state called 147 | `username` (with `React.useState`) and we'll set the `username` to whatever the 148 | input value is. We'll just lowercase the input value before doing so. Then we'll 149 | pass that value to the `input`'s `value` prop and now it's impossible for users 150 | to enter an invalid value! 151 | 152 | ## 🦉 Feedback 153 | 154 | Fill out 155 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=06%3A%20Forms&em=). 156 | -------------------------------------------------------------------------------- /src/exercise/07.js: -------------------------------------------------------------------------------- 1 | // Rendering Lists 2 | // http://localhost:3000/isolated/exercise/07.js 3 | 4 | import * as React from 'react' 5 | 6 | const allItems = [ 7 | {id: 'apple', value: '🍎 apple'}, 8 | {id: 'orange', value: '🍊 orange'}, 9 | {id: 'grape', value: '🍇 grape'}, 10 | {id: 'pear', value: '🍐 pear'}, 11 | ] 12 | 13 | function App() { 14 | const [items, setItems] = React.useState(allItems) 15 | 16 | function addItem() { 17 | const itemIds = items.map(i => i.id) 18 | setItems([...items, allItems.find(i => !itemIds.includes(i.id))]) 19 | } 20 | 21 | function removeItem(item) { 22 | setItems(items.filter(i => i.id !== item.id)) 23 | } 24 | 25 | return ( 26 |
27 | 30 |
    31 | {items.map(item => ( 32 | // 🐨 add a key prop to the
  • below. Set it to item.id 33 |
  • 34 | {' '} 35 | {' '} 36 | 37 |
  • 38 | ))} 39 |
40 |
41 | ) 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /src/exercise/07.md: -------------------------------------------------------------------------------- 1 | # Rendering Arrays 2 | 3 | ## 📝 Your Notes 4 | 5 | Elaborate on your learnings here in `src/exercise/07.md` 6 | 7 | ## Background 8 | 9 | One of the more tricky things with React is the requirement of a `key` prop when 10 | you attempt to render a list of elements. 11 | 12 | If we want to render a list like this, then there's no problem: 13 | 14 | ```jsx 15 | const ui = ( 16 |
    17 |
  • One
  • 18 |
  • Two
  • 19 |
  • Three
  • 20 |
21 | ) 22 | ``` 23 | 24 | But rendering an array of elements is very common: 25 | 26 | ```jsx 27 | const list = ['One', 'Two', 'Three'] 28 | 29 | const ui = ( 30 |
    31 | {list.map(listItem => ( 32 |
  • {listItem}
  • 33 | ))} 34 |
35 | ) 36 | ``` 37 | 38 | Those will generate the same HTML, but what it actually does is slightly 39 | different. Let's re-write it to see that difference: 40 | 41 | ```jsx 42 | const list = ['One', 'Two', 'Three'] 43 | const listUI = list.map(listItem =>
  • {listItem}
  • ) 44 | // notice that listUI is an array 45 | const ui =
      {listUI}
    46 | ``` 47 | 48 | So we're interpolating an array of renderable elements. This is totally 49 | acceptable, but it has interesting implications for when things change over 50 | time. 51 | 52 | If you re-render that list with an added item, React doesn't really know whether 53 | you added an item in the middle, beginning, or end. And the same goes for when 54 | you remove an item (it doesn't know whether that happened in the middle, 55 | beginning, or end either). 56 | 57 | In this example, it's not a big deal, because React's best-guess is right and it 58 | works out ok. However, if any of those React elements represent a component that 59 | is maintaining state, that can be pretty problematic, which this exercise 60 | demonstrates. 61 | 62 | ## Exercise 63 | 64 | Production deploys: 65 | 66 | - [Exercise](http://react-fundamentals.netlify.app/isolated/exercise/07.js) 67 | - [Final](http://react-fundamentals.netlify.app/isolated/final/07.js) 68 | 69 | We've got a problem. You may have already noticed the error message in the 70 | console about it. Try this: 71 | 72 | 1. Hit the "remove" button on the last list item 73 | 2. Notice that list item is now gone 👍 74 | 3. Hit the "remove" button on the first list item 75 | 4. Notice that everything's mixed up! 😦 76 | 77 | Let me describe what's going on here. 78 | 79 | Here's the TL;DR: Every React element accepts a special `key` prop you can use 80 | to help React keep track of elements between updates. If you don't provide it 81 | when rendering a list, React can get things mixed up. The solution is to give 82 | each element a unique (to the array) `key` prop, and then everything will work 83 | fine. 84 | 85 | Let's dive in a little deeper: 86 | 87 | If you re-render that list with an added item, React doesn't really know whether 88 | you added an item in the middle, beginning, or end. And the same goes for when 89 | you remove an item (it doesn't know whether that happened in the middle, 90 | beginning, or end either). 91 | 92 | To be clear, _we_ know as the developer because we wrote the code, but as far as 93 | React is concerned, we simply gave it some react elements before, we gave it 94 | some after, and now React is trying to compare the before and after with no 95 | knowledge of how the elements got from one position to another. 96 | 97 | Sometimes it's not a big deal, because React's best-guess is right and it works 98 | out ok. However, if any of those React elements represent a component that is 99 | maintaining state (like the value of an input or focus state), that can be 100 | pretty problematic, which this exercise demonstrates. 101 | 102 | To solve this problem, we need to give React a hint so it can associate the old 103 | React elements with the new ones we're giving it due to the change. We do this 104 | using a special prop called the `key` prop. 105 | 106 | In this exercise, we have a list of fruit that appear and can be removed. There 107 | is state that exists (managed by the browser) in the `` for each of the 108 | fruit: the input's `value` (initialized via the `defaultValue` prop). 109 | 110 | Without a `key` prop, for all React knows, you removed an input and gave another 111 | label different text content, which leads to the bug we'll see in the exercise. 112 | 113 | So here's the rule: 114 | 115 | **Whenever you're rendering an array of React elements, each one must have a 116 | unique `key` prop.** 117 | 118 | 📜 You can learn more about what can go wrong when you don't specify the `key` 119 | prop in my blog post 120 | [Understanding React's key prop](https://kentcdodds.com/blog/understanding-reacts-key-prop). 121 | 122 | 📜 Also, you can get a deeper understanding in this blog post: 123 | [Why React needs a key prop](https://epicreact.dev/why-react-needs-a-key-prop). 124 | That'll give you a bit of what's going on under the hood, so I recommend reading 125 | this! 126 | 127 | 🐨 The React elements we're rendering are the `li` elements, so for this 128 | exercise, add a `key` prop there. You can use the `item.id` for the value to 129 | ensure that the key value is unique for each element. 130 | 131 | 🦉 Note, the key only needs to be unique within a given array. So this works 132 | fine: 133 | 134 | ```tsx 135 | const element = ( 136 |
      137 | {list.map(listItem => ( 138 |
    • {listItem.value}
    • 139 | ))} 140 | {list.map(listItem => ( 141 |
    • {listItem.value}
    • 142 | ))} 143 |
    144 | ) 145 | ``` 146 | 147 | 🦉 In our example, the `value` of the input is managed by the browser, but this 148 | has even bigger implications when we start working with our own state and 149 | side-effects. It's a little too early to demonstrate this for you, but you 150 | should know that when React removes a component from the DOM, it gets 151 | "unmounted" which will trigger side-effect cleanups, and if new elements are 152 | added then those will be "mounted" and will trigger your side-effects. This can 153 | cause some surprising and problematic issues for your users. So just remember 154 | the rule and always provide a `key` when rendering an array. Later when you have 155 | more React experience, you can come back to this exercise and expand it a bit 156 | with custom components that manage state and side-effects to observe the 157 | problems caused when you ignore the `key`. 158 | 159 | ## Extra Credit 160 | 161 | ### 1. 💯 Focus Demo 162 | 163 | [Production deploy](http://react-fundamentals.netlify.app/isolated/final/07.extra-1.js) 164 | 165 | 🐨 For this extra credit, open the production deploy above. 166 | 167 | You can observe that when we're talking about "state" we're also talking about 168 | keyboard focus as well as what text is selected! As you play around with this, 169 | try selecting text in the inputs and observe how the first two examples differ 170 | from the last one. You'll notice that using the array `index` as a key is no 171 | different from React's default behavior, so it's unlikely to fix issues if 172 | you're having them. Best to use a unique ID. Play around with it! (Remember, 173 | you'll find the source for this demo in the `final` directory). 174 | 175 | There are some other interesting things you can do with `key`s as well (like 176 | changing them on an element to intentionally reset the state of a component). 177 | Feel free to play around with that if you like, but we'll be using that in a 178 | future workshop so look forward to it! 179 | 180 | ## 🦉 Feedback 181 | 182 | Fill out 183 | [the feedback form](https://ws.kcd.im/?ws=React%20Fundamentals%20%E2%9A%9B&e=07%3A%20Rendering%20Arrays&em=). 184 | -------------------------------------------------------------------------------- /src/final/01.extra-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /src/final/01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | 13 | 14 | -------------------------------------------------------------------------------- /src/final/02.extra-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 21 | 22 | -------------------------------------------------------------------------------- /src/final/02.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | 7 | 8 | 16 | 17 | -------------------------------------------------------------------------------- /src/final/03.extra-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /src/final/03.extra-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /src/final/03.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | 7 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/final/04.extra-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 25 | 26 | -------------------------------------------------------------------------------- /src/final/04.extra-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 25 | 26 | -------------------------------------------------------------------------------- /src/final/04.extra-3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 45 | 46 | -------------------------------------------------------------------------------- /src/final/04.extra-4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 11 | 33 | 34 | -------------------------------------------------------------------------------- /src/final/04.extra-5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 | 9 | 10 | 11 | 33 | 34 | -------------------------------------------------------------------------------- /src/final/04.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /src/final/05.extra-1.js: -------------------------------------------------------------------------------- 1 | // Styling 2 | // 💯 Create a custom component 3 | // http://localhost:3000/isolated/final/05.extra-1.js 4 | 5 | import * as React from 'react' 6 | import '../box-styles.css' 7 | 8 | function Box({style, className = '', ...otherProps}) { 9 | return ( 10 |
    15 | ) 16 | } 17 | 18 | function App() { 19 | return ( 20 |
    21 | 22 | small lightblue box 23 | 24 | 25 | medium pink box 26 | 27 | 28 | large orange box 29 | 30 | sizeless box 31 |
    32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/final/05.extra-2.js: -------------------------------------------------------------------------------- 1 | // Styling 2 | // 💯 accept a size prop to encapsulate styling 3 | // http://localhost:3000/isolated/final/05.extra-2.js 4 | 5 | import * as React from 'react' 6 | import '../box-styles.css' 7 | 8 | function Box({style, size, className = '', ...otherProps}) { 9 | const sizeClassName = size ? `box--${size}` : '' 10 | return ( 11 |
    16 | ) 17 | } 18 | 19 | function App() { 20 | return ( 21 |
    22 | 23 | small lightblue box 24 | 25 | 26 | medium pink box 27 | 28 | 29 | large orange box 30 | 31 | sizeless box 32 |
    33 | ) 34 | } 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /src/final/05.js: -------------------------------------------------------------------------------- 1 | // Styling 2 | // http://localhost:3000/isolated/final/05.js 3 | 4 | import * as React from 'react' 5 | import '../box-styles.css' 6 | 7 | const smallBox = ( 8 |
    12 | small lightblue box 13 |
    14 | ) 15 | const mediumBox = ( 16 |
    20 | medium pink box 21 |
    22 | ) 23 | const largeBox = ( 24 |
    28 | large orange box 29 |
    30 | ) 31 | 32 | function App() { 33 | return ( 34 |
    35 | {smallBox} 36 | {mediumBox} 37 | {largeBox} 38 |
    39 | ) 40 | } 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /src/final/06.extra-1.js: -------------------------------------------------------------------------------- 1 | // Basic Forms 2 | // 💯 using refs 3 | // http://localhost:3000/isolated/final/06.extra-1.js 4 | 5 | import * as React from 'react' 6 | 7 | function UsernameForm({onSubmitUsername}) { 8 | const usernameInputRef = React.useRef() 9 | 10 | function handleSubmit(event) { 11 | event.preventDefault() 12 | onSubmitUsername(usernameInputRef.current.value) 13 | } 14 | 15 | return ( 16 | 17 |
    18 | 19 | 20 |
    21 | 22 | 23 | ) 24 | } 25 | 26 | function App() { 27 | const onSubmitUsername = username => alert(`You entered: ${username}`) 28 | return 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /src/final/06.extra-2.js: -------------------------------------------------------------------------------- 1 | // Dynamic Forms 2 | // 💯 Validate lower-case 3 | // http://localhost:3000/isolated/final/06.extra-2.js 4 | 5 | import * as React from 'react' 6 | 7 | function UsernameForm({onSubmitUsername}) { 8 | const [error, setError] = React.useState(null) 9 | 10 | function handleSubmit(event) { 11 | event.preventDefault() 12 | onSubmitUsername(event.target.elements.usernameInput.value) 13 | } 14 | 15 | function handleChange(event) { 16 | const {value} = event.target 17 | const isLowerCase = value === value.toLowerCase() 18 | setError(isLowerCase ? null : 'Username must be lower case') 19 | } 20 | 21 | return ( 22 |
    23 |
    24 | 25 | 26 |
    27 |
    28 | {error} 29 |
    30 | 33 |
    34 | ) 35 | } 36 | 37 | function App() { 38 | const onSubmitUsername = username => alert(`You entered: ${username}`) 39 | return ( 40 |
    41 | 42 |
    43 | ) 44 | } 45 | 46 | export default App 47 | -------------------------------------------------------------------------------- /src/final/06.extra-3.js: -------------------------------------------------------------------------------- 1 | // Controlled Forms 2 | // 💯 Control the input value 3 | // http://localhost:3000/isolated/final/06.extra-3.js 4 | 5 | import * as React from 'react' 6 | 7 | function UsernameForm({onSubmitUsername}) { 8 | const [username, setUsername] = React.useState('') 9 | 10 | function handleSubmit(event) { 11 | event.preventDefault() 12 | onSubmitUsername(username) 13 | } 14 | 15 | function handleChange(event) { 16 | setUsername(event.target.value.toLowerCase()) 17 | } 18 | 19 | return ( 20 |
    21 |
    22 | 23 | 29 |
    30 | 31 |
    32 | ) 33 | } 34 | 35 | function App() { 36 | const onSubmitUsername = username => alert(`You entered: ${username}`) 37 | return ( 38 |
    39 | 40 |
    41 | ) 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /src/final/06.js: -------------------------------------------------------------------------------- 1 | // Basic Forms 2 | // http://localhost:3000/isolated/final/06.js 3 | 4 | import * as React from 'react' 5 | 6 | function UsernameForm({onSubmitUsername}) { 7 | function handleSubmit(event) { 8 | event.preventDefault() 9 | onSubmitUsername(event.target.elements.usernameInput.value) 10 | } 11 | 12 | return ( 13 |
    14 |
    15 | 16 | 17 |
    18 | 19 |
    20 | ) 21 | } 22 | 23 | function App() { 24 | const onSubmitUsername = username => alert(`You entered: ${username}`) 25 | return 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /src/final/07.extra-1.js: -------------------------------------------------------------------------------- 1 | // Rendering Lists 2 | // 💯 Focus Demo 3 | // http://localhost:3000/isolated/final/07.extra-1.js 4 | 5 | import * as React from 'react' 6 | 7 | function FocusDemo() { 8 | const [items, setItems] = React.useState([ 9 | {id: 'apple', value: '🍎 apple'}, 10 | {id: 'orange', value: '🍊 orange'}, 11 | {id: 'grape', value: '🍇 grape'}, 12 | {id: 'pear', value: '🍐 pear'}, 13 | ]) 14 | 15 | React.useEffect(() => { 16 | const id = setInterval(() => setItems(shuffle), 1000) 17 | return () => clearInterval(id) 18 | }, []) 19 | 20 | function getChangeHandler(item) { 21 | return event => { 22 | const newValue = event.target.value 23 | setItems(allItems => 24 | allItems.map(i => ({ 25 | ...i, 26 | value: i.id === item.id ? newValue : i.value, 27 | })), 28 | ) 29 | } 30 | } 31 | 32 | return ( 33 |
    34 |
    35 |

    Without a key

    36 | {items.map(item => ( 37 | 42 | ))} 43 |
    44 |
    45 |

    With array index as key

    46 | {items.map((item, index) => ( 47 | 53 | ))} 54 |
    55 |
    56 |

    With a Proper Key

    57 | {items.map(item => ( 58 | 64 | ))} 65 |
    66 |
    67 | ) 68 | } 69 | 70 | function shuffle(originalArray) { 71 | const array = [...originalArray] 72 | let currentIndex = array.length 73 | let temporaryValue 74 | let randomIndex 75 | // While there remain elements to shuffle... 76 | while (0 !== currentIndex) { 77 | // Pick a remaining element... 78 | randomIndex = Math.floor(Math.random() * currentIndex) 79 | currentIndex -= 1 80 | // And swap it with the current element. 81 | temporaryValue = array[currentIndex] 82 | array[currentIndex] = array[randomIndex] 83 | array[randomIndex] = temporaryValue 84 | } 85 | return array 86 | } 87 | 88 | function App() { 89 | return 90 | } 91 | 92 | export default App 93 | -------------------------------------------------------------------------------- /src/final/07.js: -------------------------------------------------------------------------------- 1 | // Rendering Lists 2 | // http://localhost:3000/isolated/final/07.js 3 | 4 | import * as React from 'react' 5 | 6 | const allItems = [ 7 | {id: 'apple', value: '🍎 apple'}, 8 | {id: 'orange', value: '🍊 orange'}, 9 | {id: 'grape', value: '🍇 grape'}, 10 | {id: 'pear', value: '🍐 pear'}, 11 | ] 12 | 13 | function App() { 14 | const [items, setItems] = React.useState(allItems) 15 | 16 | function addItem() { 17 | const itemIds = items.map(i => i.id) 18 | setItems([...items, allItems.find(i => !itemIds.includes(i.id))]) 19 | } 20 | 21 | function removeItem(item) { 22 | setItems(items.filter(i => i.id !== item.id)) 23 | } 24 | 25 | return ( 26 |
    27 | 30 |
      31 | {items.map(item => ( 32 |
    • 33 | {' '} 34 | {' '} 35 | 36 |
    • 37 | ))} 38 |
    39 |
    40 | ) 41 | } 42 | 43 | export default App 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import codegen from 'codegen.macro' 3 | 4 | codegen`module.exports = require('@kentcdodds/react-workshop-app/codegen')` 5 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@kentcdodds/react-workshop-app/setup-tests' 2 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .keys input { 2 | margin-right: 10px; 3 | margin-left: 10px; 4 | } 5 | .keys input:focus { 6 | outline: 6px solid #4bd2fb; 7 | } 8 | .keys .apple-input, 9 | .keys [for='apple-input'], 10 | .keys #apple-input { 11 | background-color: #ff9393; 12 | } 13 | .keys .orange-input, 14 | .keys [for='orange-input'], 15 | .keys #orange-input { 16 | background-color: #f1b66d; 17 | } 18 | .keys .grape-input, 19 | .keys [for='grape-input'], 20 | .keys #grape-input { 21 | background-color: #e27ce2; 22 | } 23 | .keys .pear-input, 24 | .keys [for='pear-input'], 25 | .keys #pear-input { 26 | background-color: #bef171; 27 | } 28 | 29 | .keys li { 30 | margin-top: 10px; 31 | margin-bottom: 10px; 32 | } 33 | 34 | .keys [for='apple-input'], 35 | .keys [for='orange-input'], 36 | .keys [for='grape-input'], 37 | .keys [for='pear-input'] { 38 | display: inline-block; 39 | width: 100px; 40 | text-align: right; 41 | } 42 | --------------------------------------------------------------------------------