From 52ab8b189c015cdb13c4842d73e2662d1f97ccb3 Mon Sep 17 00:00:00 2001 From: Steven Hicks Date: Sat, 9 Mar 2019 20:17:21 -0600 Subject: [PATCH] Truffle shuffle - reorder exercises. --- exercise-10/README.md | 118 ++------ exercise-10/SOLUTIONS.md | 63 ---- {exercise-13 => exercise-10}/a_callbacks.js | 0 .../a_callbacks.spec.js | 0 {exercise-13 => exercise-10}/b_promises.js | 0 .../b_promises.spec.js | 0 {exercise-13 => exercise-10}/c_async.js | 0 {exercise-13 => exercise-10}/c_async.spec.js | 0 {exercise-4 => exercise-10}/jest.config.js | 2 +- exercise-11/App.js | 7 +- exercise-11/README.md | 158 ++++++++--- exercise-11/SOLUTIONS.md | 121 ++++---- exercise-11/complete/App.js | 4 +- .../complete/data/db.json | 0 exercise-11/complete/data/friends.js | 9 +- .../friend-detail/FriendDetail.entry.js | 23 +- .../complete/friend-detail/FriendDetail.js | 22 +- .../friend-detail/FriendDetail.module.css | 10 + .../complete/friend-detail/FriendFlipper.js | 0 .../friend-detail/FriendFlipper.module.css | 0 .../friend-detail/get-friend-from-api.js | 0 exercise-11/complete/friends/Friends.entry.js | 20 +- .../complete/friends/get-friends-from-api.js | 0 exercise-11/complete/shared/Card.module.css | 3 - {exercise-14 => exercise-11}/data/db.json | 0 exercise-11/data/friends.js | 9 +- .../friend-detail/FriendDetail.entry.js | 7 +- exercise-11/friend-detail/FriendDetail.js | 14 +- .../friend-detail/FriendDetail.module.css | 10 + .../friend-detail/FriendFlipper.js | 0 .../friend-detail/FriendFlipper.module.css | 0 .../friend-detail/get-friend-from-api.js | 0 exercise-11/friends/FriendProfile.js | 17 +- .../friends/get-friends-from-api.js | 0 exercise-11/shared/Card.module.css | 3 - exercise-12/App.js | 7 +- exercise-12/App.module.css | 15 - {exercise-15 => exercise-12}/Header.js | 0 .../Header.module.css | 0 exercise-12/README.md | 248 +++++++++++----- exercise-12/SOLUTIONS.md | 157 +++++++--- {exercise-15 => exercise-12}/complete/App.js | 0 .../complete}/App.module.css | 0 .../complete/Header.js | 0 .../complete/Header.module.css | 0 .../friend-detail/FriendDetail.entry.js | 0 .../complete/friend-detail/FriendDetail.js | 0 .../friend-detail/FriendDetail.module.css | 0 .../complete/friend-detail/FriendFlipper.js | 80 +++--- .../friend-detail/FriendFlipper.module.css | 17 +- .../friend-detail/get-friend-from-api.js | 0 .../complete/friends/FriendProfile.js | 0 .../friends}/FriendProfile.module.css | 0 .../complete/friends/Friends.entry.js | 0 .../complete/friends/Friends.js | 0 .../complete/friends/get-friends-from-api.js | 0 .../complete/shared/Card.js | 0 .../complete/shared/Card.module.css | 0 .../complete/shared/Page.js | 0 .../complete/shared/Page.module.css | 0 .../complete/theme/Provider.js | 0 .../complete/theme/Switcher.js | 0 .../complete/theme/Switcher.module.css | 0 .../complete/theme/context.js | 0 .../complete/theme/static.js | 0 {exercise-15 => exercise-12}/data/db.json | 0 .../friend-detail/FriendDetail.entry.js | 23 +- exercise-12/friend-detail/FriendDetail.js | 21 +- exercise-12/friend-detail/FriendFlipper.js | 35 ++- .../friend-detail/FriendFlipper.module.css | 17 +- .../friend-detail/get-friend-from-api.js | 0 exercise-12/friends/Friends.entry.js | 20 +- .../friends/get-friends-from-api.js | 0 exercise-12/shared/Card.js | 6 +- exercise-12/shared/Card.module.css | 11 +- exercise-12/shared/Page.js | 4 +- exercise-12/shared/Page.module.css | 7 +- .../theme/Provider.js | 0 .../theme/Switcher.js | 0 .../theme/Switcher.module.css | 0 {exercise-15 => exercise-12}/theme/context.js | 0 {exercise-15 => exercise-12}/theme/static.js | 0 exercise-13/README.md | 10 +- exercise-13/jest.config.js | 2 +- {exercise-3 => exercise-13}/things.js | 0 {exercise-3 => exercise-13}/things.spec.js | 0 {exercise-7 => exercise-14}/App.css | 0 exercise-14/App.js | 28 +- {exercise-7 => exercise-14}/Exercise.js | 0 exercise-14/README.md | 169 ++++------- exercise-14/SOLUTIONS.md | 108 ++----- .../complete/Exercise.js | 0 .../friend-detail/FriendDetail.entry.js | 22 -- .../complete/friend-detail/FriendDetail.js | 34 --- exercise-15/App.js | 26 -- exercise-15/README.md | 243 +--------------- exercise-15/SOLUTIONS.md | 141 --------- .../complete/friend-detail/FriendFlipper.js | 86 ------ exercise-15/friends/Friends.entry.js | 22 -- exercise-15/shared/Card.js | 11 - exercise-16/README.md | 231 +-------------- exercise-16/complete/App.module.css | 7 - .../friend-detail/FriendDetail.module.css | 13 - .../friend-detail/FriendDetail.entry.js | 22 -- .../friend-detail/FriendDetail.module.css | 13 - exercise-16/friends/FriendProfile.js | 19 -- exercise-16/friends/Friends.entry.js | 22 -- exercise-16/shared/Card.module.css | 13 - exercise-2/README.md | 5 + exercise-3/README.md | 11 +- {exercise-4 => exercise-3}/greetings.js | 0 {exercise-4 => exercise-3}/greetings.spec.js | 0 exercise-4/App.css | 40 +++ exercise-4/App.js | 17 ++ exercise-4/Exercise.js | 35 +++ exercise-4/README.md | 268 ++++++++++++++++-- exercise-4/SOLUTIONS.md | 124 ++++++++ exercise-4/complete/Exercise.js | 50 ++++ {exercise-10 => exercise-4}/index.css | 0 {exercise-15 => exercise-4}/index.js | 0 exercise-5/App.css | 21 +- exercise-5/App.js | 8 +- exercise-5/Exercise.js | 22 +- exercise-5/README.md | 262 +++++------------ exercise-5/SOLUTIONS.md | 108 ++----- exercise-5/complete/Exercise.js | 35 +-- exercise-6/App.css | 38 ++- exercise-6/App.js | 4 +- exercise-6/Exercise.js | 22 +- exercise-6/README.md | 163 +++++------ exercise-6/SOLUTIONS.md | 66 ++--- exercise-6/complete/Exercise.js | 51 +++- .../docs/pages-and-cards.png | Bin {exercise-8 => exercise-6}/docs/pages.png | Bin .../1-component-css-files/App.css | 0 .../1-component-css-files/App.js | 0 .../1-component-css-files/Card.css | 0 .../1-component-css-files/Card.js | 0 .../1-component-css-files/Exercise.js | 0 .../1-component-css-files/FriendProfile.css | 0 .../1-component-css-files/FriendProfile.js | 0 .../1-component-css-files/Friends.js | 0 .../1-component-css-files/Page.css | 0 .../1-component-css-files/Page.js | 0 .../2-css-modules/App.js | 0 .../2-css-modules/App.module.css | 0 .../2-css-modules/Card.js | 0 .../2-css-modules/Card.module.css | 0 .../2-css-modules/Exercise.js | 0 .../2-css-modules/FriendProfile.js | 0 .../2-css-modules}/FriendProfile.module.css | 0 .../2-css-modules/Friends.js | 0 .../2-css-modules/Page.js | 0 .../2-css-modules/Page.module.css | 0 .../3-styled-components/App.js | 0 .../3-styled-components/Card.js | 0 .../3-styled-components/Exercise.js | 0 .../3-styled-components/FriendProfile.js | 0 .../3-styled-components/Friends.js | 0 .../3-styled-components/Page.js | 0 .../3-styled-components/theme.js | 0 {exercise-10 => exercise-7}/Page.js | 0 exercise-7/README.md | 129 +++++---- exercise-7/SOLUTIONS.md | 100 ++++--- exercise-7/index.js | 4 +- exercise-8/App.css | 55 ---- exercise-8/App.js | 25 +- {exercise-14 => exercise-8}/App.module.css | 0 exercise-8/Exercise.js | 39 --- exercise-8/README.md | 134 +++------ exercise-8/SOLUTIONS.md | 80 ++++-- {exercise-14 => exercise-8}/complete/App.js | 4 +- .../complete/App.module.css | 0 exercise-8/complete/Exercise.js | 60 ---- .../complete}/data/friends.js | 9 +- .../friend-detail/FriendDetail.entry.js | 0 .../complete}/friend-detail/FriendDetail.js | 21 +- .../friend-detail/FriendDetail.module.css | 3 + .../complete}/friends/FriendProfile.js | 0 .../friends/FriendProfile.module.css | 0 .../complete}/friends/Friends.entry.js | 0 .../complete}/friends/Friends.js | 0 .../complete/shared/Card.js | 0 .../complete}/shared/Card.module.css | 3 + .../complete/shared/Page.js | 0 .../complete/shared/Page.module.css | 0 .../complete => exercise-8}/data/friends.js | 9 +- .../friend-detail/FriendDetail.entry.js | 12 + exercise-8/friend-detail/FriendDetail.js | 20 ++ .../friend-detail/FriendDetail.module.css | 3 + exercise-8/friends/FriendProfile.js | 16 ++ .../friends/FriendProfile.module.css | 0 exercise-8/friends/Friends.entry.js | 10 + .../friends/Friends.js | 0 {exercise-14 => exercise-8}/shared/Card.js | 0 exercise-8/shared/Card.module.css | 11 + {exercise-14 => exercise-8}/shared/Page.js | 0 .../shared/Page.module.css | 0 exercise-9/App.js | 28 +- exercise-9/App.module.css | 22 ++ exercise-9/README.md | 157 ++++++---- exercise-9/SOLUTIONS.md | 92 +++--- .../complete}/friend-detail/FriendFlipper.js | 8 +- .../friend-detail/FriendFlipper.module.css | 17 +- {exercise-12 => exercise-9}/data/friends.js | 0 .../friend-detail/FriendDetail.entry.js | 13 + .../friend-detail/FriendDetail.js | 0 .../friend-detail/FriendDetail.module.css | 0 exercise-9/friend-detail/FriendFlipper.js | 53 ++++ .../friend-detail/FriendFlipper.module.css | 17 +- .../friends/FriendProfile.js | 0 .../friends/FriendProfile.module.css | 0 exercise-9/friends/Friends.entry.js | 10 + .../friends/Friends.js | 0 exercise-9/shared/Card.js | 7 + .../shared/Card.module.css | 0 {exercise-15 => exercise-9}/shared/Page.js | 4 +- .../shared/Page.module.css | 7 +- {exercise-16 => exercise-xx-hooks}/App.js | 0 .../App.module.css | 0 {exercise-16 => exercise-xx-hooks}/Header.js | 0 .../Header.module.css | 0 exercise-xx-hooks/README.md | 232 +++++++++++++++ .../SOLUTIONS.md | 0 .../complete/App.js | 0 .../complete}/App.module.css | 0 .../complete/Header.js | 0 .../complete/Header.module.css | 0 .../friend-detail/FriendDetail.entry.js | 0 .../complete/friend-detail/FriendDetail.js | 0 .../friend-detail/FriendDetail.module.css | 0 .../complete/friend-detail/FriendFlipper.js | 0 .../friend-detail/FriendFlipper.module.css | 0 .../friend-detail/FriendFlipperBack.js | 0 .../friend-detail/FriendFlipperFront.js | 0 .../friend-detail/get-friend-from-api.js | 0 .../complete}/friends/FriendProfile.js | 0 .../complete/friends/FriendProfile.module.css | 0 .../complete/friends/Friends.entry.js | 0 .../complete/friends/Friends.js | 0 .../complete/friends/get-friends-from-api.js | 0 .../complete/shared/Card.js | 0 .../complete}/shared/Card.module.css | 0 .../complete/shared/Page.js | 0 .../complete}/shared/Page.module.css | 0 .../complete/theme/Provider.js | 0 .../complete/theme/Switcher.js | 0 .../complete/theme/Switcher.module.css | 0 .../complete/theme/context.js | 0 .../complete/theme/static.js | 0 .../data/db.json | 0 .../friend-detail/FriendDetail.entry.js | 0 .../friend-detail/FriendDetail.js | 0 .../friend-detail/FriendDetail.module.css | 0 .../friend-detail/FriendFlipper.js | 0 .../friend-detail/FriendFlipper.module.css | 0 .../friend-detail/FriendFlipperBack.js | 0 .../friend-detail/FriendFlipperFront.js | 0 .../friend-detail/get-friend-from-api.js | 0 .../friends/FriendProfile.js | 0 .../friends/FriendProfile.module.css | 0 .../friends/Friends.entry.js | 0 .../friends/Friends.js | 0 .../friends/get-friends-from-api.js | 0 {exercise-15 => exercise-xx-hooks}/index.css | 0 {exercise-16 => exercise-xx-hooks}/index.js | 0 .../shared/Card.js | 0 .../shared/Card.module.css | 0 .../shared/Page.js | 0 .../shared/Page.module.css | 0 .../theme/Provider.js | 0 .../theme/Switcher.js | 0 .../theme/Switcher.module.css | 0 .../theme/context.js | 0 .../theme/static.js | 0 .../App.css | 0 {exercise-7 => exercise-xx-prop-types}/App.js | 4 +- .../Exercise.js | 0 exercise-xx-prop-types/README.md | 95 +++++++ exercise-xx-prop-types/SOLUTIONS.md | 54 ++++ .../complete/Exercise.js | 0 .../index.css | 0 .../index.js | 4 +- 283 files changed, 2736 insertions(+), 2715 deletions(-) delete mode 100644 exercise-10/SOLUTIONS.md rename {exercise-13 => exercise-10}/a_callbacks.js (100%) rename {exercise-13 => exercise-10}/a_callbacks.spec.js (100%) rename {exercise-13 => exercise-10}/b_promises.js (100%) rename {exercise-13 => exercise-10}/b_promises.spec.js (100%) rename {exercise-13 => exercise-10}/c_async.js (100%) rename {exercise-13 => exercise-10}/c_async.spec.js (100%) rename {exercise-4 => exercise-10}/jest.config.js (96%) rename {exercise-14 => exercise-11}/complete/data/db.json (100%) rename {exercise-14 => exercise-11}/complete/friend-detail/FriendFlipper.js (100%) rename {exercise-14 => exercise-11}/complete/friend-detail/FriendFlipper.module.css (100%) rename {exercise-14 => exercise-11}/complete/friend-detail/get-friend-from-api.js (100%) rename {exercise-14 => exercise-11}/complete/friends/get-friends-from-api.js (100%) rename {exercise-14 => exercise-11}/data/db.json (100%) rename {exercise-14 => exercise-11}/friend-detail/FriendFlipper.js (100%) rename {exercise-14 => exercise-11}/friend-detail/FriendFlipper.module.css (100%) rename {exercise-14 => exercise-11}/friend-detail/get-friend-from-api.js (100%) rename {exercise-14 => exercise-11}/friends/get-friends-from-api.js (100%) rename {exercise-15 => exercise-12}/Header.js (100%) rename {exercise-15 => exercise-12}/Header.module.css (100%) rename {exercise-15 => exercise-12}/complete/App.js (100%) rename {exercise-15 => exercise-12/complete}/App.module.css (100%) rename {exercise-15 => exercise-12}/complete/Header.js (100%) rename {exercise-15 => exercise-12}/complete/Header.module.css (100%) rename {exercise-15 => exercise-12}/complete/friend-detail/FriendDetail.entry.js (100%) rename {exercise-15 => exercise-12}/complete/friend-detail/FriendDetail.js (100%) rename {exercise-14 => exercise-12}/complete/friend-detail/FriendDetail.module.css (100%) rename {exercise-15 => exercise-12}/complete/friend-detail/get-friend-from-api.js (100%) rename {exercise-14 => exercise-12}/complete/friends/FriendProfile.js (100%) rename {exercise-10/2-css-modules => exercise-12/complete/friends}/FriendProfile.module.css (100%) rename {exercise-14 => exercise-12}/complete/friends/Friends.entry.js (100%) rename {exercise-14 => exercise-12}/complete/friends/Friends.js (100%) rename {exercise-15 => exercise-12}/complete/friends/get-friends-from-api.js (100%) rename {exercise-15 => exercise-12}/complete/shared/Card.js (100%) rename {exercise-15 => exercise-12}/complete/shared/Card.module.css (100%) rename {exercise-15 => exercise-12}/complete/shared/Page.js (100%) rename {exercise-15 => exercise-12}/complete/shared/Page.module.css (100%) rename {exercise-15 => exercise-12}/complete/theme/Provider.js (100%) rename {exercise-15 => exercise-12}/complete/theme/Switcher.js (100%) rename {exercise-15 => exercise-12}/complete/theme/Switcher.module.css (100%) rename {exercise-15 => exercise-12}/complete/theme/context.js (100%) rename {exercise-15 => exercise-12}/complete/theme/static.js (100%) rename {exercise-15 => exercise-12}/data/db.json (100%) rename {exercise-15 => exercise-12}/friend-detail/get-friend-from-api.js (100%) rename {exercise-15 => exercise-12}/friends/get-friends-from-api.js (100%) rename {exercise-15 => exercise-12}/theme/Provider.js (100%) rename {exercise-15 => exercise-12}/theme/Switcher.js (100%) rename {exercise-15 => exercise-12}/theme/Switcher.module.css (100%) rename {exercise-15 => exercise-12}/theme/context.js (100%) rename {exercise-15 => exercise-12}/theme/static.js (100%) rename {exercise-3 => exercise-13}/things.js (100%) rename {exercise-3 => exercise-13}/things.spec.js (100%) rename {exercise-7 => exercise-14}/App.css (100%) rename {exercise-7 => exercise-14}/Exercise.js (100%) rename {exercise-7 => exercise-14}/complete/Exercise.js (100%) delete mode 100644 exercise-14/complete/friend-detail/FriendDetail.entry.js delete mode 100644 exercise-14/complete/friend-detail/FriendDetail.js delete mode 100644 exercise-15/App.js delete mode 100644 exercise-15/SOLUTIONS.md delete mode 100644 exercise-15/complete/friend-detail/FriendFlipper.js delete mode 100644 exercise-15/friends/Friends.entry.js delete mode 100644 exercise-15/shared/Card.js delete mode 100644 exercise-16/complete/App.module.css delete mode 100644 exercise-16/complete/friend-detail/FriendDetail.module.css delete mode 100644 exercise-16/friend-detail/FriendDetail.entry.js delete mode 100644 exercise-16/friend-detail/FriendDetail.module.css delete mode 100644 exercise-16/friends/FriendProfile.js delete mode 100644 exercise-16/friends/Friends.entry.js delete mode 100644 exercise-16/shared/Card.module.css create mode 100644 exercise-2/README.md rename {exercise-4 => exercise-3}/greetings.js (100%) rename {exercise-4 => exercise-3}/greetings.spec.js (100%) create mode 100644 exercise-4/App.css create mode 100644 exercise-4/App.js create mode 100644 exercise-4/Exercise.js create mode 100644 exercise-4/SOLUTIONS.md create mode 100644 exercise-4/complete/Exercise.js rename {exercise-10 => exercise-4}/index.css (100%) rename {exercise-15 => exercise-4}/index.js (100%) rename {exercise-8 => exercise-6}/docs/pages-and-cards.png (100%) rename {exercise-8 => exercise-6}/docs/pages.png (100%) rename {exercise-10 => exercise-7}/1-component-css-files/App.css (100%) rename {exercise-10 => exercise-7}/1-component-css-files/App.js (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Card.css (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Card.js (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Exercise.js (100%) rename {exercise-10 => exercise-7}/1-component-css-files/FriendProfile.css (100%) rename {exercise-10 => exercise-7}/1-component-css-files/FriendProfile.js (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Friends.js (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Page.css (100%) rename {exercise-10 => exercise-7}/1-component-css-files/Page.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/App.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/App.module.css (100%) rename {exercise-10 => exercise-7}/2-css-modules/Card.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/Card.module.css (100%) rename {exercise-10 => exercise-7}/2-css-modules/Exercise.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/FriendProfile.js (100%) rename {exercise-14/complete/friends => exercise-7/2-css-modules}/FriendProfile.module.css (100%) rename {exercise-10 => exercise-7}/2-css-modules/Friends.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/Page.js (100%) rename {exercise-10 => exercise-7}/2-css-modules/Page.module.css (100%) rename {exercise-10 => exercise-7}/3-styled-components/App.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/Card.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/Exercise.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/FriendProfile.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/Friends.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/Page.js (100%) rename {exercise-10 => exercise-7}/3-styled-components/theme.js (100%) rename {exercise-10 => exercise-7}/Page.js (100%) delete mode 100644 exercise-8/App.css rename {exercise-14 => exercise-8}/App.module.css (100%) delete mode 100644 exercise-8/Exercise.js rename {exercise-14 => exercise-8}/complete/App.js (83%) rename {exercise-14 => exercise-8}/complete/App.module.css (100%) delete mode 100644 exercise-8/complete/Exercise.js rename {exercise-14 => exercise-8/complete}/data/friends.js (67%) rename {exercise-14 => exercise-8/complete}/friend-detail/FriendDetail.entry.js (100%) rename {exercise-15 => exercise-8/complete}/friend-detail/FriendDetail.js (54%) create mode 100644 exercise-8/complete/friend-detail/FriendDetail.module.css rename {exercise-14 => exercise-8/complete}/friends/FriendProfile.js (100%) rename {exercise-14 => exercise-8/complete}/friends/FriendProfile.module.css (100%) rename {exercise-14 => exercise-8/complete}/friends/Friends.entry.js (100%) rename {exercise-14 => exercise-8/complete}/friends/Friends.js (100%) rename {exercise-14 => exercise-8}/complete/shared/Card.js (100%) rename {exercise-14 => exercise-8/complete}/shared/Card.module.css (71%) rename {exercise-14 => exercise-8}/complete/shared/Page.js (100%) rename {exercise-14 => exercise-8}/complete/shared/Page.module.css (100%) rename {exercise-14/complete => exercise-8}/data/friends.js (67%) create mode 100644 exercise-8/friend-detail/FriendDetail.entry.js create mode 100644 exercise-8/friend-detail/FriendDetail.js create mode 100644 exercise-8/friend-detail/FriendDetail.module.css create mode 100644 exercise-8/friends/FriendProfile.js rename {exercise-15/complete => exercise-8}/friends/FriendProfile.module.css (100%) create mode 100644 exercise-8/friends/Friends.entry.js rename {exercise-15/complete => exercise-8}/friends/Friends.js (100%) rename {exercise-14 => exercise-8}/shared/Card.js (100%) create mode 100644 exercise-8/shared/Card.module.css rename {exercise-14 => exercise-8}/shared/Page.js (100%) rename {exercise-14 => exercise-8}/shared/Page.module.css (100%) create mode 100644 exercise-9/App.module.css rename {exercise-15 => exercise-9/complete}/friend-detail/FriendFlipper.js (85%) rename {exercise-16 => exercise-9}/complete/friend-detail/FriendFlipper.module.css (80%) rename {exercise-12 => exercise-9}/data/friends.js (100%) create mode 100644 exercise-9/friend-detail/FriendDetail.entry.js rename {exercise-14 => exercise-9}/friend-detail/FriendDetail.js (100%) rename {exercise-14 => exercise-9}/friend-detail/FriendDetail.module.css (100%) create mode 100644 exercise-9/friend-detail/FriendFlipper.js rename {exercise-16 => exercise-9}/friend-detail/FriendFlipper.module.css (80%) rename {exercise-15/complete => exercise-9}/friends/FriendProfile.js (100%) rename {exercise-15 => exercise-9}/friends/FriendProfile.module.css (100%) create mode 100644 exercise-9/friends/Friends.entry.js rename {exercise-15 => exercise-9}/friends/Friends.js (100%) create mode 100644 exercise-9/shared/Card.js rename {exercise-14/complete => exercise-9}/shared/Card.module.css (100%) rename {exercise-15 => exercise-9}/shared/Page.js (59%) rename {exercise-16 => exercise-9}/shared/Page.module.css (62%) rename {exercise-16 => exercise-xx-hooks}/App.js (100%) rename {exercise-15/complete => exercise-xx-hooks}/App.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/Header.js (100%) rename {exercise-16 => exercise-xx-hooks}/Header.module.css (100%) create mode 100644 exercise-xx-hooks/README.md rename {exercise-16 => exercise-xx-hooks}/SOLUTIONS.md (100%) rename {exercise-16 => exercise-xx-hooks}/complete/App.js (100%) rename {exercise-16 => exercise-xx-hooks/complete}/App.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/Header.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/Header.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/FriendDetail.entry.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/FriendDetail.js (100%) rename {exercise-15 => exercise-xx-hooks}/complete/friend-detail/FriendDetail.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/FriendFlipper.js (100%) rename {exercise-15 => exercise-xx-hooks}/complete/friend-detail/FriendFlipper.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/FriendFlipperBack.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/FriendFlipperFront.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friend-detail/get-friend-from-api.js (100%) rename {exercise-15 => exercise-xx-hooks/complete}/friends/FriendProfile.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friends/FriendProfile.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friends/Friends.entry.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friends/Friends.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/friends/get-friends-from-api.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/shared/Card.js (100%) rename {exercise-15 => exercise-xx-hooks/complete}/shared/Card.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/shared/Page.js (100%) rename {exercise-15 => exercise-xx-hooks/complete}/shared/Page.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/theme/Provider.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/theme/Switcher.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/theme/Switcher.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/complete/theme/context.js (100%) rename {exercise-16 => exercise-xx-hooks}/complete/theme/static.js (100%) rename {exercise-16 => exercise-xx-hooks}/data/db.json (100%) rename {exercise-15 => exercise-xx-hooks}/friend-detail/FriendDetail.entry.js (100%) rename {exercise-16 => exercise-xx-hooks}/friend-detail/FriendDetail.js (100%) rename {exercise-15 => exercise-xx-hooks}/friend-detail/FriendDetail.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/friend-detail/FriendFlipper.js (100%) rename {exercise-15 => exercise-xx-hooks}/friend-detail/FriendFlipper.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/friend-detail/FriendFlipperBack.js (100%) rename {exercise-16 => exercise-xx-hooks}/friend-detail/FriendFlipperFront.js (100%) rename {exercise-16 => exercise-xx-hooks}/friend-detail/get-friend-from-api.js (100%) rename {exercise-16/complete => exercise-xx-hooks}/friends/FriendProfile.js (100%) rename {exercise-16 => exercise-xx-hooks}/friends/FriendProfile.module.css (100%) rename {exercise-15/complete => exercise-xx-hooks}/friends/Friends.entry.js (100%) rename {exercise-16 => exercise-xx-hooks}/friends/Friends.js (100%) rename {exercise-16 => exercise-xx-hooks}/friends/get-friends-from-api.js (100%) rename {exercise-15 => exercise-xx-hooks}/index.css (100%) rename {exercise-16 => exercise-xx-hooks}/index.js (100%) rename {exercise-16 => exercise-xx-hooks}/shared/Card.js (100%) rename {exercise-16/complete => exercise-xx-hooks}/shared/Card.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/shared/Page.js (100%) rename {exercise-16/complete => exercise-xx-hooks}/shared/Page.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/theme/Provider.js (100%) rename {exercise-16 => exercise-xx-hooks}/theme/Switcher.js (100%) rename {exercise-16 => exercise-xx-hooks}/theme/Switcher.module.css (100%) rename {exercise-16 => exercise-xx-hooks}/theme/context.js (100%) rename {exercise-16 => exercise-xx-hooks}/theme/static.js (100%) rename {exercise-9 => exercise-xx-prop-types}/App.css (100%) rename {exercise-7 => exercise-xx-prop-types}/App.js (73%) rename {exercise-9 => exercise-xx-prop-types}/Exercise.js (100%) create mode 100644 exercise-xx-prop-types/README.md create mode 100644 exercise-xx-prop-types/SOLUTIONS.md rename {exercise-9 => exercise-xx-prop-types}/complete/Exercise.js (100%) rename {exercise-16 => exercise-xx-prop-types}/index.css (100%) rename {exercise-10 => exercise-xx-prop-types}/index.js (51%) diff --git a/exercise-10/README.md b/exercise-10/README.md index 14630b6..c762d81 100644 --- a/exercise-10/README.md +++ b/exercise-10/README.md @@ -1,111 +1,35 @@ # Exercise 10 -## Three Ways To Style +## Modern JS: Async/Await -There are many ways to style a React app. In this exercise, you'll compare and contrast three different ways. +This exercise introduces you to the async/await features utilized in modern JavaScript applications. -👉 Start the app for Exercise 10 +The instructor will lead you through some code examples. Follow along! -In a console window, pointed at the root of this project, run `npm run start-exercise-10`. +### Setup -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 10: Styling - Component CSS Files", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. +This exercise utilizes unit tests for demonstration purposes. You don't need to know much about unit tests coming in. -### Background +👉 Start your test suite. Open a new command window at the root of this project, and enter `npm run test-exercise-10`. -One thing is common among most ways to style a React app: they involve co-locating your CSS with your components. +You should see the following output: -In this exercise, we've styled the app three different ways - with component CSS files, CSS Modules, and Styled Components. +``` +No tests found related to files changed since last commit. +Press `a` to run all tests, or run Jest with `--watchAll`. -### Component CSS Files +Watch Usage + › Press a to run all tests. + › Press p to filter by a filename regex pattern. + › Press t to filter by a test name regex pattern. + › Press q to quit watch mode. + › Press Enter to trigger a test run. +``` -The component CSS file method of styling an app involves putting styles for a component in an associated CSS file. +If you don't see this output, try to investigate the message you see, ask your neighbor, or raise your hand for assistance. -#### Explore Component CSS Files +If you do see this output, you're in good shape. The output will change as we modify our code. -👉 Open the `/1-component-css-files` folder. +### Follow Along -In it, you'll see that each component `___.js` has an associated `___.css` file. Each of these css files contains the styles for its component, and its component only. - -👉 Open a component `.js` file and its corresponding `.css` file. - -👉 Answer some questions. - -1. How does a component CSS file get included in our app? [See here for answer](SOLUTIONS.md#1-1) - -2. How does this method of including a dependency differ from the other dependencies included in a component file? [See here for answer](SOLUTIONS.md#1-2) - -### CSS Modules - -CSS Modules attempt to solve a frustrating problem that many developers struggle with - CSS scope conflicts. - -CSS Modules look like a similar strategy to Component CSS Files, in that there is a single CSS file associated with each component. However, a CSS Module gets compiled into a set of styles that are locally scoped, using a unique hash for every component. When a CSS Module is compiled, it becomes a JavaScript object, with properties for every style defined in the CSS Module. A JavaScript file that imports a CSS Module can reference the styles in the module via properties on the import. - -If you'd like to read more about CSS Modules, visit https://github.com/css-modules/css-modules. - -#### Switch to a CSS Module implementation - -👉 In `index.js`, comment out line 4 (which tells the app to use component CSS files), and uncomment line 5 (which tells the app to use CSS Modules). - -You should see the color of the app change from green to purple in your browser, and the subtitle should change to indicate it is using CSS Modules. - -#### Explore CSS Modules - -👉 Open the `/2-css-modules` folder. - -Inside this folder you'll see that again, there is a single `.css` file for each component. - -Notice that every CSS file name ends with `.module.css`. This is an implementation detail of create-react-app, the tool that was used to generate the app. This naming convention is not required to use CSS Modules in general, but is required to use them with create-react-app. - -👉 Open a component `.js` file and its corresponding `.module.css` file. - -👉 Answer some questions. - -1. How does a CSS Module get included in our app? How is this different from a component CSS file? [See here for answer](SOLUTIONS.md#2-1) - -2. How is a style in a CSS Module used in a component? [See here for answer](SOLUTIONS.md#2-2) - -3. On line 13 of `App.js`, there is a call to a function named `classNames`. What do you think this statement is doing? [See here for answer](SOLUTIONS.md#2-3) - -4. In your browser, inspect an element that is styled with a CSS Module. What do you notice about the class associated with the element, and why do you think that is? [See here for answer](SOLUTIONS.md#2-4) - -### Styled Components - -Styled Components are very different from the first two approaches. Instead of having a separate CSS file for every component, all styling happens inside of each component - as JavaScript. - -To define styles for a component with Styled Components, you create a React component that defines nothing but styles, then render it as a child of your component. - -Each Styled Component is compiled, and uses local scope like CSS Modules - so it also solves the frustrating problem of CSS scope conflicts. - -#### Switch to a Styled Components implementation - -👉 In `index.js`, comment out line 5 (which tells the app to use CSS Modules), and uncomment line 6 (which tells the app to use Styled Components). - -You should see the color of the app change from purple to green in your browser, and the subtitle should change to indicate it is using Styled Components. - -#### Explore Styled Components - -👉 Open the `/3-styled-components` folder. - -Inside this folder you'll see that there are no `.css` files. This is because all component styles are within each `.js` file. - -👉 Open the `App.js` file. - -On line 2, we `import styled from 'styled-components'`. This is the library we use to build Styled Components. - -The `styled` object exposes one helper for any HTML element that you would want to style. - -On lines 7-9, we use the `styled.div` helper to create a `div` element with style `text-align: center`. But immediately following the word `div` is a pair of backticks (`), with what looks like CSS inside them. - -This is called a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates). Tagged templates are a newer feature of JavaScript that allow you to parse a template literal with a function. In this example, the function is the `div` helper of `styled`. The template literal being parsed is the content between the backticks immediately following the word `div`. - -The contents of the template literal for a Styled Component are the CSS that you want to apply to the element. - -We looked at template literals earlier in the workshop. Remember that you can inject values in a template literal with `${...}`. This means we can inject any JavaScript into the CSS for an element! - -👉 Answer some questions. - -1. How is the `AppDiv` component used? How do you think this element will render in the browser? [See here for answer](SOLUTIONS.md#3-1) - -2. In your browser, use the dev tools to find the `AppDiv` element. What does the class name look like? [See here for answer](SOLUTIONS.md#3-2) - -3. On line 14 of `App.js`, in the `AppHeader` component, we are using string interpolation to inject the value `theme.white`. What do you think is the intention of this code? [See here for answer](SOLUTIONS.md#3-3) +The instructor will lead you in comparing different methods of writing asynchronous unit tests. diff --git a/exercise-10/SOLUTIONS.md b/exercise-10/SOLUTIONS.md deleted file mode 100644 index 22860a6..0000000 --- a/exercise-10/SOLUTIONS.md +++ /dev/null @@ -1,63 +0,0 @@ -# Answers - -## 1. Component CSS Files - -### 1-1 - -CSS files are included in each component using `import '___.css'`. - -When the app is compiled with webpack, this import indicates that the component has a dependency on the CSS file, and it will include it in the build. - -### 1-2 - -Imported JavaScript dependencies include an identifier for the module being imported (i.e. `import dependency from 'dependency'), while imported CSS files do not (i.e.`import 'styles.css'`). - -This is because JavaScript dependencies are only imported if your code needs to interact with them. CSS dependencies aren't interacted with, so they don't need an identifier. - -## 2. CSS Modules - -### 2-1 - -A CSS Module is imported into a component using `import styles from '___.module.css'`. This is different from component CSS files in that we are giving the imported module an identifier - `styles`. - -### 2-2 - -This `styles` object imported from the CSS Module contains properties for every class we define in the `styles.css` file. We use that object to specify which style from the module to use on an element. - -For example, if the CSS Module contained a style `.title { ... }`, in the associated component we would use it with `

`. - -### 2-3 - -The `classNames` library is used to combine CSS Module class names. - -In the example specified, we are saying that this element should use both the `subTitle` and `emphasize` styles. - -We could probably combine these names ourselves, without the `classNames` library. `classNames` is a handy helper, though, and can also be used to combine styles conditionally. If you're using CSS Modules in a project, there's a good chance you'll also want to use `classNames`. - -### 2-4 - -The class name looks generated, and contains a hash, like this - `Card_card__9mB3O`. - -This is because CSS Modules is compiling our styles, and it gives each class a unique name to avoid conflicts. - -## Styled Components - -### 3-1 - -On line 24, we render the `AppDiv` element in our `App` component, like this: - -``` - - ... - -``` - -We can do this, because a Styled Component is just another React component! - -### 3-2 - -Similarly to the CSS Module class, the Styled Component class name contains a hash. As you might expect, this is so that the class is uniquely named, and we don't have to deal with style conflicts. - -### 3-3 - -The theme colors are defined in a separate JavaScript file, and we are injecting them here! This would be a great way to manage themes in one place, and have them consumed throughout the app. If we decide the theme colors need to change, we can change them in one place, instead of a bunch of CSS files! diff --git a/exercise-13/a_callbacks.js b/exercise-10/a_callbacks.js similarity index 100% rename from exercise-13/a_callbacks.js rename to exercise-10/a_callbacks.js diff --git a/exercise-13/a_callbacks.spec.js b/exercise-10/a_callbacks.spec.js similarity index 100% rename from exercise-13/a_callbacks.spec.js rename to exercise-10/a_callbacks.spec.js diff --git a/exercise-13/b_promises.js b/exercise-10/b_promises.js similarity index 100% rename from exercise-13/b_promises.js rename to exercise-10/b_promises.js diff --git a/exercise-13/b_promises.spec.js b/exercise-10/b_promises.spec.js similarity index 100% rename from exercise-13/b_promises.spec.js rename to exercise-10/b_promises.spec.js diff --git a/exercise-13/c_async.js b/exercise-10/c_async.js similarity index 100% rename from exercise-13/c_async.js rename to exercise-10/c_async.js diff --git a/exercise-13/c_async.spec.js b/exercise-10/c_async.spec.js similarity index 100% rename from exercise-13/c_async.spec.js rename to exercise-10/c_async.spec.js diff --git a/exercise-4/jest.config.js b/exercise-10/jest.config.js similarity index 96% rename from exercise-4/jest.config.js rename to exercise-10/jest.config.js index e24945e..0619eaf 100644 --- a/exercise-4/jest.config.js +++ b/exercise-10/jest.config.js @@ -1,4 +1,4 @@ -const exercise = 'exercise-4'; +const exercise = 'exercise-13'; module.exports = { rootDir: '../', diff --git a/exercise-11/App.js b/exercise-11/App.js index cd0b14f..5ef2e8b 100644 --- a/exercise-11/App.js +++ b/exercise-11/App.js @@ -3,6 +3,8 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import Friends from './friends/Friends.entry'; +import FriendDetail from './friend-detail/FriendDetail.entry'; + import styles from './App.module.css'; function App() { @@ -10,11 +12,12 @@ function App() {
-

Exercise 11

-

React Router

+

Exercise 14

+

Loading Data

+
diff --git a/exercise-11/README.md b/exercise-11/README.md index b0a7ec7..1286256 100644 --- a/exercise-11/README.md +++ b/exercise-11/README.md @@ -1,86 +1,158 @@ # Exercise 11 -## React Router +## Loading Data -In this exercise, we're going to use React-Router to build a second page into our app. When we're complete, users will be able to navigate from our friends list to a detailed view of each friend. +This exercise introduces you to the usual method of loading data from an API in a React component. 👉 Start the app for Exercise 11 In a console window, pointed at the root of this project, run `npm run start-exercise-11`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 11: React Router", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 11: Loading Data", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. -### Component Reorganization +### The `friends` API -In this exercise, we've re-architected our file & folder structure. We've created some folders: +Prior to this exercise, we were using a static list of friends, imported from the file `data/friends.js`. We're going to instead retrieve our data from a simple API based on the contents of `data/db.json`. -- `/data`: this folder holds the data that drives our app. For now, that's our list of friends. -- `/friend-detail`: this folder holds components that render a "friend detail" page. This is a new page we'll be building out in this exercise. -- `/friends`: this folder holds components that render the "friends list" page - the one we've been working with so far. -- `/shared`: this folder contains components that could be shared across multiple other folders. +The API is already running. To see it in action, you can navigate to an endpoint in your browser. -### Router Setup +👉 Browse to the URL `http://localhost:3000/api/friends`. -To set up React-Router, we've already taken a few steps: +You should see a JSON response that contains our three friends. -- Installed React-Router as a dependency, by running `npm install --save react-router` -- Wrapped our app in a `` component, in `App.js` -- Defined a route in `App.js` that matches the path `/` exactly, and renders our `` entry component. +If you change any contents in `data/db.json`, the `friends` endpoint will reflect it. (Though you will have to refresh the page to see the updates.) -### Add a new `` +### Lifecycle Events -In the folder `/friend-detail`, we've added a couple components that render a very simple Friend Detail page. +Several lifecycle events are involved when loading data from an API: initialization, `render`, and `componentDidMount`. -👉 Add a new `` to `App.js`, which will render the new Friend Detail page. +#### Initialization -You'll want to import the `` component from './friend-detail/FriendDetail.entry'. +The state to be loaded from an API is initialized to an empty value. -The route should match the path `friends/:id`. The token `:id` indicates that this route will have an `id` parameter submitted in the path. +For a refresher on how to initialize state, see [exercise 10](../exercise-10/README.md#initializing-state). -If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetail-route). +#### `render` -### Add a link to the new Route +The state data is rendered in the `render` function of a component. When the component initially loads, it renders with an empty value (`undefined`, or `null`, or an empty array, or whatever you choose). -We need a way to navigate to the route we added. +After the data is completed loading from the API, the component renders with the updated state. -React-Router includes a `` component for navigating to a router-friendly URL. +#### componentDidMount -👉 Wrap the `` element in `'/friends/FriendProfile.js` in a `` element. +The `componentDidMount` lifecycle event fires right after a React component is added to the DOM. From within `componentDidMount`, we'll call the API endpoint. -Remember that you'll need to import the `Link` component from `react-router-dom`, and that the `to` prop is where the link will go when it is clicked. +When the API call is complete, we can use `setState()` to update the state with the loaded data. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-link). +Recall that to easily handle asynchronous processing in a component lifecycle event, you can simply mark the method with `async`, and call `await` within it. -### Get friend ID from the URL +#### An example -When you click on one of our kitten friends, you should navigate to the Friend Detail page! +Following is a simple example of how the lifecycle events work together to load data into a component. -Unfortunately, regardless of which kitten you click, it always renders the same friend - Turtle. +```jsx +export default class MyComponent extends React.Component { + // Initialization + state = { + items: [], + }; -👉 Open `/friend-detail/FriendDetail.entry.js`, and see if you can identify why we are always rendering Turtle. + // Render the state + render() { + return ; + } -...(come back when you've figured it out, or you give up)... + // Load the state from an API + async componentDidMount() { + const items = await loadItemsFromApi(); + this.setState({ + items, + }); + } +} +``` -As the comment in the component indicates, we aren't getting the active friend ID in this component. +### Loading the FriendsEntry data from the API -When we use React-Router, our components automatically get access to a prop named `match`. This `match` prop contains a `params` array, which contains all the parameters passed into the current route. +The first component we'll update to pull from the API is the `FriendsEntry` component, located at `friends/Friends.entry.js`. -👉 Modify `/friend-detail/FriendDetail.entry.js` so that it pulls the active friend ID from the `match` prop. +#### get-friends-from-api.js -Use `console.log` to inspect the props passed into the component if you need to. +We've included a function in `friends/get-friends-from-api.js`, which will make the API call to collect all of our friends. It uses the `axios` library to make an HTTP call to the `friends` API endpoint. -You'll want to use `friends.find(...)` to find the friend whose id property matches the ID passed in. +👉 Import the `getFriendsFromApi` function into `friends/Friends.entry.js`. -One other trick - the `id` parameter in the `match.params` array is a string; the `id` properties in the `friends` array are integers. You could use `parseInt(...)` to convert between the two types. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-import-api). -When you've completed this task, you should be able to navigate to all three kitten friends' detail pages. +Before we can use our lifecycle events to connect to the API endpoint, we'll need to convert our component to a stateful one. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#active-friend-id). +TODO: nope. -### Return to Home +👉 Convert the `FriendsEntry` component from a stateless functional component to a stateful class syntax component. -We can use the browser navigation to go back to the Home page, but it'd be really helpful if we could add a link to the "Home" page on the Friend Detail page. +For a reminder on how to do this, see [exercise 7](../exercise-7/README.md#the-process). -👉 Add a Link to `/friend-detail/FriendDetail.js` that takes us back to the Home page. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-stateful). -If you get stuck, [see a possible solution here](./SOLUTIONS.md#home-link). +#### Initialize the state + +Our component renders a list of friends. We'll want to initialize our component state so that it contains an empty friends array. + +👉 Initialize the state of the `FriendsEntry` component so that it contains an empty array named `friends`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-initialize). + +#### Render the `friends` data from local state + +👉 Modify the `render()` function of the `FriendsEntry` component so that it renders the friends from `this.state.friends`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-render). + +#### Call the `friends` API to get data + +The final step for connecting the `FriendsEntry` component to an API is to load the data from within `componentDidMount()`. + +👉 Add a `componentDidMount()` method that (a) calls the API to get friend data, then (b) calls `setState()` to update the state of the component with the friend data. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-componentdidmount). + +### Loading FriendDetailEntry data from the API + +The `FriendDetailEntry` component, at `friend-detail/FriendDetail.entry.js`, also needs to load data from an API endpoint. + +#### Handling an empty friend + +In the previous activity, the `FriendsEntry` component worked with an empty array for the default state. In this activity, the `FriendDetailEntry` component will need to account for an `undefined` friend. This situation can happen when the component is still loading data from the API, and if our component can't handle an `undefined` friend, it will err out. + +A great place to handle this dichotomy is within the `FriendDetail` component, in `friends/FriendDetail.js`. + +👉 Modify the `FriendDetail` component to render an appropriately constructed page when an undefined `friend` is passed in. + +If an actual `friend` is passed in, it should continue to render the full `FriendDetail` information. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetail-handle-empty-friend). + +👉 Repeat the activity of loading data from an API for the `FriendDetailEntry` component. + +Refer to the notes above as a reminder of how to do this. There are a couple details that make this component different than the first: + +- You'll only be loading one friend this time. +- It should default to `undefined`, instead of an empty array. +- The ID for the current friend will be passed into the `FriendDetailEntry` component via the `match.params.id` prop, thanks to ReactRouter. +- The function that calls the API is in `friend-detail/get-friend-from-api.js`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetailentry). + +### Test it out + +You should now have your friends loading from API endpoints throughout the app. + +You can verify this by making a change in `data/db.json`, and making sure the change is reflected in the app. You will need to refresh the app to see the change. + +### Extra Credit + +- Show a "loading" indicator when the friends array has not yet loaded on the Friends list page. + +- Read more about [state & lifecycle events](https://reactjs.org/docs/state-and-lifecycle.html). + +- Read about another use of React lifecycle events - [integrating with non-React libraries](https://reactjs.org/docs/integrating-with-other-libraries.html). diff --git a/exercise-11/SOLUTIONS.md b/exercise-11/SOLUTIONS.md index 53d70bc..07c9b04 100644 --- a/exercise-11/SOLUTIONS.md +++ b/exercise-11/SOLUTIONS.md @@ -1,65 +1,57 @@ # Possible Solutions -## FriendDetail Route +## Friends: Import API +```js +import getFriendsFromApi from './get-friends-from-api'; +``` +## Friends: Stateful ```jsx -import { BrowserRouter, Route } from 'react-router-dom'; -import FriendDetail from './friend-detail/FriendDetail.entry'; -... - -function App() { - return ( - -
-
-

Exercise 11

-

React Router

-
-
- - -
-
-
- ); +export default class FriendsEntry extends React.Component { + render() { + return + } } - ``` -## FriendProfile Link +## Friends: Initialize ```jsx -import { Link } from 'react-router-dom'; -... +export default class FriendsEntry extends React.Component { + state = { + friends: [] + } -export default function FriendProfile({ id, name, image }) { - return ( - - -
- {name} -

{name}

-
-
- - ); + // ... } ``` -## Active friend ID +## Friends: render +```jsx +export default class FriendsEntry extends React.Component { + // ... + + render() { + return ; + } +} +``` +## Friends: componentDidMount ```jsx -export default function({ match }) { - // the match prop is passed in via react.router - const friendId = match.params.id; - const friend = friends.find(x => x.id === parseInt(friendId, 10)); +export default class FriendsEntry extends React.Component { + // ... - return ; + async componentDidMount() { + const friends = await getFriendsFromApi(); + this.setState({ + friends + }); + } } ``` -## Home Link - +## FriendDetail: Handle empty friend ```jsx export default function({ friend }) { return ( @@ -68,15 +60,44 @@ export default function({ friend }) {
< Home
- -
-

{friend.name}

- -

{friend.bio}

-
-
+ {renderFriend(friend)} ); } + +function renderFriend(friend) { + if (friend === undefined) { + return

Loading...

; + } + + return ( +
+

{friend.name}

+ +

{friend.bio}

+
+ ); +} +``` + +## FriendDetailEntry +```jsx +export default class FriendDetailEntry extends React.Component { + state = { + friend: undefined, + }; + + render() { + return ; + } + + async componentDidMount() { + // the match prop is passed in via react.router + const friend = await getFriendFromApi(this.props.match.params.id); + this.setState({ + friend, + }); + } +} ``` diff --git a/exercise-11/complete/App.js b/exercise-11/complete/App.js index d9bcaab..5ef2e8b 100644 --- a/exercise-11/complete/App.js +++ b/exercise-11/complete/App.js @@ -12,8 +12,8 @@ function App() {
-

Exercise 11

-

React Router

+

Exercise 14

+

Loading Data

diff --git a/exercise-14/complete/data/db.json b/exercise-11/complete/data/db.json similarity index 100% rename from exercise-14/complete/data/db.json rename to exercise-11/complete/data/db.json diff --git a/exercise-11/complete/data/friends.js b/exercise-11/complete/data/friends.js index 5fb4b02..7ba5db0 100644 --- a/exercise-11/complete/data/friends.js +++ b/exercise-11/complete/data/friends.js @@ -3,18 +3,21 @@ export default [ id: 1, name: 'Potatoes', image: 'http://placekitten.com/150/150?image=1', - bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.' + bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.', + colors: [ 'Gray', 'Black' ] }, { id: 2, name: 'Flower', image: 'http://placekitten.com/150/150?image=12', - bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.' + bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.', + colors: [ 'Gray', 'Black', 'White' ] }, { id: 3, name: 'Turtle', image: 'http://placekitten.com/150/150?image=15', - bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.' + bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.', + colors: [ 'Gray', 'White' ] }, ]; diff --git a/exercise-11/complete/friend-detail/FriendDetail.entry.js b/exercise-11/complete/friend-detail/FriendDetail.entry.js index 9b25248..c90c4e1 100644 --- a/exercise-11/complete/friend-detail/FriendDetail.entry.js +++ b/exercise-11/complete/friend-detail/FriendDetail.entry.js @@ -1,13 +1,22 @@ import React from 'react'; -import friends from '../data/friends'; +import FriendDetailFinished from './FriendDetail'; +import getFriendFromApi from './get-friend-from-api'; -import FriendDetail from './FriendDetail'; +export default class FriendDetailEntry extends React.Component { + state = { + friend: undefined, + }; -export default function({match}) { - // the match prop is passed in via react.router - const friendId = match.params.id; - const friend = friends.find(x => x.id === parseInt(friendId, 10)); + async componentDidMount() { + // the match prop is passed in via react.router + const friend = await getFriendFromApi(this.props.match.params.id); + this.setState({ + friend, + }); + } - return ; + render() { + return ; + } } diff --git a/exercise-11/complete/friend-detail/FriendDetail.js b/exercise-11/complete/friend-detail/FriendDetail.js index fad1d4e..11d1636 100644 --- a/exercise-11/complete/friend-detail/FriendDetail.js +++ b/exercise-11/complete/friend-detail/FriendDetail.js @@ -2,6 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import Page from '../shared/Page'; import Card from '../shared/Card'; +import FriendFlipper from './FriendFlipper'; import styles from './FriendDetail.module.css'; @@ -12,13 +13,22 @@ export default function({ friend }) {
< Home
- -

{friend.name}

-

ID: {friend.id}

- -

{friend.bio}

-
+ {renderFriend(friend)}
); } + +function renderFriend(friend) { + if (friend === undefined) { + return

Loading...

; + } + + return ( +
+

{friend.name}

+ +

{friend.bio}

+
+ ); +} diff --git a/exercise-11/complete/friend-detail/FriendDetail.module.css b/exercise-11/complete/friend-detail/FriendDetail.module.css index 3432a3e..1eac493 100644 --- a/exercise-11/complete/friend-detail/FriendDetail.module.css +++ b/exercise-11/complete/friend-detail/FriendDetail.module.css @@ -1,3 +1,13 @@ .friendDetail { width: 50%; } + +.cardContents { + display: flex; + flex-direction: column; + align-items: center; +} + +.toolbar { + margin: 1em 0; +} \ No newline at end of file diff --git a/exercise-14/complete/friend-detail/FriendFlipper.js b/exercise-11/complete/friend-detail/FriendFlipper.js similarity index 100% rename from exercise-14/complete/friend-detail/FriendFlipper.js rename to exercise-11/complete/friend-detail/FriendFlipper.js diff --git a/exercise-14/complete/friend-detail/FriendFlipper.module.css b/exercise-11/complete/friend-detail/FriendFlipper.module.css similarity index 100% rename from exercise-14/complete/friend-detail/FriendFlipper.module.css rename to exercise-11/complete/friend-detail/FriendFlipper.module.css diff --git a/exercise-14/complete/friend-detail/get-friend-from-api.js b/exercise-11/complete/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-14/complete/friend-detail/get-friend-from-api.js rename to exercise-11/complete/friend-detail/get-friend-from-api.js diff --git a/exercise-11/complete/friends/Friends.entry.js b/exercise-11/complete/friends/Friends.entry.js index df30e43..0ad1ffa 100644 --- a/exercise-11/complete/friends/Friends.entry.js +++ b/exercise-11/complete/friends/Friends.entry.js @@ -1,10 +1,22 @@ import React from 'react'; -import myFriends from '../data/friends'; +import getFriendsFromApi from './get-friends-from-api'; import Friends from './Friends'; -export default function FriendsEntry() { - return -} +export default class FriendsEntry extends React.Component { + state = { + friends: [] + } + + async componentDidMount() { + const friends = await getFriendsFromApi(); + this.setState({ + friends + }); + } + render() { + return ; + } +} diff --git a/exercise-14/complete/friends/get-friends-from-api.js b/exercise-11/complete/friends/get-friends-from-api.js similarity index 100% rename from exercise-14/complete/friends/get-friends-from-api.js rename to exercise-11/complete/friends/get-friends-from-api.js diff --git a/exercise-11/complete/shared/Card.module.css b/exercise-11/complete/shared/Card.module.css index 3265a74..3aa7162 100644 --- a/exercise-11/complete/shared/Card.module.css +++ b/exercise-11/complete/shared/Card.module.css @@ -6,6 +6,3 @@ box-shadow: 0 1px 2px var(--brand-dark); } -.card:hover { - box-shadow: 0 2px 4px var(--brand-dark); -} diff --git a/exercise-14/data/db.json b/exercise-11/data/db.json similarity index 100% rename from exercise-14/data/db.json rename to exercise-11/data/db.json diff --git a/exercise-11/data/friends.js b/exercise-11/data/friends.js index 5fb4b02..7ba5db0 100644 --- a/exercise-11/data/friends.js +++ b/exercise-11/data/friends.js @@ -3,18 +3,21 @@ export default [ id: 1, name: 'Potatoes', image: 'http://placekitten.com/150/150?image=1', - bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.' + bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.', + colors: [ 'Gray', 'Black' ] }, { id: 2, name: 'Flower', image: 'http://placekitten.com/150/150?image=12', - bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.' + bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.', + colors: [ 'Gray', 'Black', 'White' ] }, { id: 3, name: 'Turtle', image: 'http://placekitten.com/150/150?image=15', - bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.' + bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.', + colors: [ 'Gray', 'White' ] }, ]; diff --git a/exercise-11/friend-detail/FriendDetail.entry.js b/exercise-11/friend-detail/FriendDetail.entry.js index 9796960..9b25248 100644 --- a/exercise-11/friend-detail/FriendDetail.entry.js +++ b/exercise-11/friend-detail/FriendDetail.entry.js @@ -4,9 +4,10 @@ import friends from '../data/friends'; import FriendDetail from './FriendDetail'; -export default function() { - //TODO - how do we get the active friend id??? - const friend = friends[2]; +export default function({match}) { + // the match prop is passed in via react.router + const friendId = match.params.id; + const friend = friends.find(x => x.id === parseInt(friendId, 10)); return ; } diff --git a/exercise-11/friend-detail/FriendDetail.js b/exercise-11/friend-detail/FriendDetail.js index 018677e..8ae18f8 100644 --- a/exercise-11/friend-detail/FriendDetail.js +++ b/exercise-11/friend-detail/FriendDetail.js @@ -1,6 +1,8 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import Page from '../shared/Page'; import Card from '../shared/Card'; +import FriendFlipper from './FriendFlipper'; import styles from './FriendDetail.module.css'; @@ -8,11 +10,15 @@ export default function({ friend }) { return (
+
+ < Home +
-

{friend.name}

-

ID: {friend.id}

- -

{friend.bio}

+
+

{friend.name}

+ +

{friend.bio}

+
diff --git a/exercise-11/friend-detail/FriendDetail.module.css b/exercise-11/friend-detail/FriendDetail.module.css index 3432a3e..1eac493 100644 --- a/exercise-11/friend-detail/FriendDetail.module.css +++ b/exercise-11/friend-detail/FriendDetail.module.css @@ -1,3 +1,13 @@ .friendDetail { width: 50%; } + +.cardContents { + display: flex; + flex-direction: column; + align-items: center; +} + +.toolbar { + margin: 1em 0; +} \ No newline at end of file diff --git a/exercise-14/friend-detail/FriendFlipper.js b/exercise-11/friend-detail/FriendFlipper.js similarity index 100% rename from exercise-14/friend-detail/FriendFlipper.js rename to exercise-11/friend-detail/FriendFlipper.js diff --git a/exercise-14/friend-detail/FriendFlipper.module.css b/exercise-11/friend-detail/FriendFlipper.module.css similarity index 100% rename from exercise-14/friend-detail/FriendFlipper.module.css rename to exercise-11/friend-detail/FriendFlipper.module.css diff --git a/exercise-14/friend-detail/get-friend-from-api.js b/exercise-11/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-14/friend-detail/get-friend-from-api.js rename to exercise-11/friend-detail/get-friend-from-api.js diff --git a/exercise-11/friends/FriendProfile.js b/exercise-11/friends/FriendProfile.js index 5f98acd..c8c886b 100644 --- a/exercise-11/friends/FriendProfile.js +++ b/exercise-11/friends/FriendProfile.js @@ -1,16 +1,19 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import Card from '../shared/Card'; import styles from './FriendProfile.module.css'; -export default function FriendProfile({ name, image }) { +export default function FriendProfile({ id, name, image }) { return ( - -
- {name} -

{name}

-
-
+ + +
+ {name} +

{name}

+
+
+ ); } diff --git a/exercise-14/friends/get-friends-from-api.js b/exercise-11/friends/get-friends-from-api.js similarity index 100% rename from exercise-14/friends/get-friends-from-api.js rename to exercise-11/friends/get-friends-from-api.js diff --git a/exercise-11/shared/Card.module.css b/exercise-11/shared/Card.module.css index 3265a74..3aa7162 100644 --- a/exercise-11/shared/Card.module.css +++ b/exercise-11/shared/Card.module.css @@ -6,6 +6,3 @@ box-shadow: 0 1px 2px var(--brand-dark); } -.card:hover { - box-shadow: 0 2px 4px var(--brand-dark); -} diff --git a/exercise-12/App.js b/exercise-12/App.js index 3f1b388..49c74a4 100644 --- a/exercise-12/App.js +++ b/exercise-12/App.js @@ -2,6 +2,8 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; +import Header from './Header'; + import Friends from './friends/Friends.entry'; import FriendDetail from './friend-detail/FriendDetail.entry'; @@ -11,10 +13,7 @@ function App() { return (
-
-

Exercise 12

-

Managing Component State

-
+
diff --git a/exercise-12/App.module.css b/exercise-12/App.module.css index 3688af6..dbbd108 100644 --- a/exercise-12/App.module.css +++ b/exercise-12/App.module.css @@ -1,22 +1,7 @@ .app { text-align: center; - - --brand-dark: blueviolet; - --brand-light: ghostwhite; -} - -.appHeader { - background-color: var(--brand-dark); - height: 100px; - padding: 20px; - color: white; } .emphasize { text-decoration: underline; } - -.appTitle { - font-family: 'Pacifico', cursive; - line-height: 0.5em; -} diff --git a/exercise-15/Header.js b/exercise-12/Header.js similarity index 100% rename from exercise-15/Header.js rename to exercise-12/Header.js diff --git a/exercise-15/Header.module.css b/exercise-12/Header.module.css similarity index 100% rename from exercise-15/Header.module.css rename to exercise-12/Header.module.css diff --git a/exercise-12/README.md b/exercise-12/README.md index 12ccf49..95d45e4 100644 --- a/exercise-12/README.md +++ b/exercise-12/README.md @@ -1,148 +1,244 @@ # Exercise 12 -## Managing Component State +## React Context -In this exercise, we'll use component state to flip an information card back-and-forth on our FriendDetail page. +In this exercise, we'll use React Context to manage application-level state. That is, state that applies to the entire application, instead of a single component (or a few closely-related components). + +Throughout the workshop, you've seen the app alternate between green and purple themes. We're going to automate that, with a "theme switcher" button. 👉 Start the app for Exercise 12 In a console window, pointed at the root of this project, run `npm run start-exercise-12`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 12: Managing Component State", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 12: React Context", and our three adorable kitten friends in the purple theme. If it doesn't, ask your neighbor for assistance or raise your hand. -### FriendFlipper: A New Component +### Static theme -We've redesigned our FriendDetail page a bit. It references a new child component - the ``. +We've modified the CSS Modules coming into this exercise, so that they render based on a static theme. The static theme is defined in `./theme/static/index.js`. If you change the static theme, the UI will reflect it. -The `` component will flip an information card for the user. The front is an image of our friend; on the back will be some details, like their ID & colors. The flipper isn't functional yet - but it has all the styles & content built out for it. +👉 Change the exported value in `./theme/static/index.js` from `purple` to `green`. -Your responsibility will be to utilize React component state to toggle the visible side of the flipper. +In your browser, you should see the theme change from purple to green. -### Initializing State +### ThemeSwitcher -The first thing we need to do with a stateful component is initialize the state. +In `Header.js`, you'll see that we've rendered a new component named ``. This shows up in the UI as a button that says "Change Theme". We're going to hook theme-toggling up to this `ThemeSwitcher` component, using React Context. -In this case, we'll want our initial state to be such that the card is not flipped yet. We will use a boolean state property named `flipped` to manage this. +### React Context -Remember that when initializing state, we can do it a couple ways: +The React Context API involves three players: a Context, a Provider, and many Consumers. -1. Constructor initialization +The Context defines that we will use a specific type of context in our app. -This method utilizes the class constructor, and looks something like this: +### ThemeContext -```jsx -export default class Component extends React.Component { - constructor(props) { - super(props); // This calls the base constructor, with the passed-in props. +For this exercise, we've already created the Context. It is named `ThemeContext`, and is located at `theme/context.js`. - this.state = { - // set initial state properties here - }; - } - // ... -} +👉 Open the `theme/context.js` file. + +You should see this: + +```js +import React from 'react'; + +export default React.createContext(); ``` -2. Class property initialization +There isn't much happening here. The important thing is that it is calling `React.createContext()`, and exporting the result. When we want to create a Provider or Consumer, we'll need to use this Context. -This method utilizes a newer feature of JavaScript - class properties - and looks something like this: +### ThemeProvider + +A Provider handles the state management for a Context. + +For this exercise, we've already created the Provider. It is named `ThemeProvider`, and it's located at `theme/Provider.js`. + +👉 Open the `theme/Provider.js` file. + +You should see a component that looks similar to other stateful components. It includes several pieces we've seen in previous exercises. + +#### State Initialization + +The `ThemeProvider` initializes its state, so that the `theme` is defaulted to `purple`. ```jsx -export default class Component extends React.Component { +export default class ThemeProvider extends React.Component { state = { - myValue: 0, // or whatever default you want to set it to + theme: 'purple', }; // ... } ``` -👉 Initialize the state in `friend-detail/FriendFlipper.js` so that the `flipped` property is defaulted to `false`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#initialization). - -### Adding An Event Handler +#### Handle State Change -We need to handle the event that a user "flips" the information card. +The `ThemeProvider` includes a `handleTimeChange()` method, which updates the state of the Context using `setState`. -Recall that we handle an event in a stateful component with a class property assigned to a fat-arrow method that calls `setState`. This looks something like this: +This handler is swapping the value of `this.state.theme` between `green` and `purple` each time it is called. ```jsx -export default class Component extends React.Component { - myEventHandler = () => { - this.setState({ - myValue: 1, // or whatever value you want to set it to - }); +export default class ThemeProvider extends React.Component { + // ... + + handleThemeChange = () => { + this.setState(prevState => ({ + theme: prevState.theme === 'green' ? 'purple' : 'green', + })); }; + // ... } ``` -If we want to update the state based on the current state of the component, we can use the alternate override for `setState`, like this: +#### render() + +The last thing the `ThemeProvider` does is render the current state. + +A Context Provider passes the state down to its children via the `value` prop. The `value` prop can contain a single value, or it can contain an object if there are multiple values to pass down. + +In this case, we need to pass down the current theme, as well as our handler that toggles the theme from `green` to `purple`. Thus we create an object (`data`), and pass that into the rendered ``, instead of a single value. ```jsx -export default class Component extends React.Component { - myEventHandler = () => { - this.setState(prevProps => { - return { - myValue: prevProps.myValue + 1, // or whatever value you want to set it to - }; - }); - }; +export default class ThemeProvider extends React.Component { // ... + + render() { + const data = { + theme: this.state.theme, + onThemeChanged: this.handleThemeChange, + }; + + return ( + + {this.props.children} + + ); + } } ``` -👉 Add an event handler to `friend-detail/FriendFlipper.js` that updates the `flipped` state property to the opposite of the current value of `flipped`. +Inside the `` is rendered the children passed into the provider. This allows the Provider to wrap a component tree, and pass the state down to all of its children. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#event-handler). +### Wrap the app in a `` -### Conditionally flipping the card based on state +Having created a `ThemeProvider`, we need to wrap our component tree within it, so that it may pass the state throughout the app. -Once our event handler changes the state, we'll need our `render` function to flip the card based on the `flipped` state property. +👉 Wrap the app in a `` component, within `App.js`. -We can do this by conditionally calling `renderFront()` or `renderBack()` in our `render` function. +We want our entire app to have access to the ThemeContext, so we'll wrap the entire app in the `ThemeProvider`. -👉 Conditionally call `renderFront()` or `renderBack()` in `friend-detail/FriendFlipper.js`, based on the value of `this.state.flipped`. +You'll want to import the `ThemeProvider`, and wrap the rendered `App` component within a `` element. -If `this.state.flipped` is true, `renderBack()` should be called. If `this.state.flipped` is false, `renderFront()` should be called. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#app-themeprovider). -You can use ternaries to call the appropriate `render___()` function, or call a function that uses an if/else. +### Adding ThemeContext.Consumers -If you get stuck, [see a possible solution here](./SOLUTIONS.md#conditional-render). +Now that we've wrapped the app in the `ThemeProvider`, we can attach Consumers anywhere down the tree. -### Tying it all together - connecting the buttons to the event handler +Consumers look like this: -We have one last step to hook up our state management. When the buttons on the information card are clicked, they need to call our event handler. +```jsx +import ThemeContext from './theme/context'; + +export default function MyComponent() { + return ( + + {value => } + + ); +} +``` -Calling event handlers from a button click looks like this: +The child of the `` is a function. It takes an argument that contains the `value` that was passed down by the `ThemeContext.Provider`. -```jsx -class MyComponent extends React.Component { - myEventHandler = () => { - // ... - }; +Inside the inner function, your components can do whatever they need with the value passed in. - render() { - // ... - + )} + + ); +} +``` + +## Switcher: onThemeChanged +```jsx +import ThemeContext from './context'; - ... +export default function() { + return ( + + {({ theme, onThemeChanged }) => ( + + )} + + ); } ``` -## Conditional Render +## Header: Consumer ```jsx -export default class FriendFlipper extends React.Component { - ... - render() { - return ( -
-
- {this.state.flipped ? null : this.renderFront()} - {!this.state.flipped ? null : this.renderBack()} +// import theme from './theme/static'; // no longer needed +import ThemeContext from './theme/context'; + +export default function Header() { + return ( + + {({ theme }) => ( +
+ ... +
+ ); +} +``` + +## Page: Consumer +```jsx +import ThemeContext from '../theme/context'; + +export default function Page({ children }) { + return ( + + {({ theme }) => ( +
+
{children}
-
- ); - } - ... + )} + + ); } ``` -## Connect Buttons To Handler +## Card: Consumer ```jsx - -
-
+ + {({ theme }) => ( +
+
+ {friend.image} + +
+
+ )} +
); } renderBack() { const { friend } = this.props; return ( -
-
- {friend.image} -
-

- ID: - {friend.id} -

-

Colors:

-
    - {friend.colors.map(color => ( -
  • {color}
  • - ))} -
+ + {({ theme }) => ( +
+
+ {friend.image} +
+

+ ID: + {friend.id} +

+

Colors:

+
    + {friend.colors.map(color => ( +
  • {color}
  • + ))} +
+
+ +
- -
-
+ )} + ); } } diff --git a/exercise-12/complete/friend-detail/FriendFlipper.module.css b/exercise-12/complete/friend-detail/FriendFlipper.module.css index da93169..8135352 100644 --- a/exercise-12/complete/friend-detail/FriendFlipper.module.css +++ b/exercise-12/complete/friend-detail/FriendFlipper.module.css @@ -9,6 +9,7 @@ .flipper { position: relative; + background-color: gray; } /* hide back of pane during swap */ @@ -22,14 +23,21 @@ box-shadow: 0 1px 2px; } +/* front pane, placed above back */ + .flipperNav { - background: var(--brand-dark); border: none; padding: 5px 10px; color: white; text-decoration: underline; cursor: pointer; } +.flipperNav.green { + background: teal; +} +.flipperNav.purple { + background: blueviolet; +} .flipperNav:focus { opacity: 0.8; outline: none; @@ -41,12 +49,17 @@ } .backContents { font-size: 0.8rem; - background-color: var(--brand-dark); color: white; text-align: left; height: calc(100% - 10px); padding: 5px; } +.backContents.green { + background: teal; +} +.backContents.purple { + background: blueviolet; +} .backContents .flipperNav { position: absolute; bottom: 5px; diff --git a/exercise-15/complete/friend-detail/get-friend-from-api.js b/exercise-12/complete/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-15/complete/friend-detail/get-friend-from-api.js rename to exercise-12/complete/friend-detail/get-friend-from-api.js diff --git a/exercise-14/complete/friends/FriendProfile.js b/exercise-12/complete/friends/FriendProfile.js similarity index 100% rename from exercise-14/complete/friends/FriendProfile.js rename to exercise-12/complete/friends/FriendProfile.js diff --git a/exercise-10/2-css-modules/FriendProfile.module.css b/exercise-12/complete/friends/FriendProfile.module.css similarity index 100% rename from exercise-10/2-css-modules/FriendProfile.module.css rename to exercise-12/complete/friends/FriendProfile.module.css diff --git a/exercise-14/complete/friends/Friends.entry.js b/exercise-12/complete/friends/Friends.entry.js similarity index 100% rename from exercise-14/complete/friends/Friends.entry.js rename to exercise-12/complete/friends/Friends.entry.js diff --git a/exercise-14/complete/friends/Friends.js b/exercise-12/complete/friends/Friends.js similarity index 100% rename from exercise-14/complete/friends/Friends.js rename to exercise-12/complete/friends/Friends.js diff --git a/exercise-15/complete/friends/get-friends-from-api.js b/exercise-12/complete/friends/get-friends-from-api.js similarity index 100% rename from exercise-15/complete/friends/get-friends-from-api.js rename to exercise-12/complete/friends/get-friends-from-api.js diff --git a/exercise-15/complete/shared/Card.js b/exercise-12/complete/shared/Card.js similarity index 100% rename from exercise-15/complete/shared/Card.js rename to exercise-12/complete/shared/Card.js diff --git a/exercise-15/complete/shared/Card.module.css b/exercise-12/complete/shared/Card.module.css similarity index 100% rename from exercise-15/complete/shared/Card.module.css rename to exercise-12/complete/shared/Card.module.css diff --git a/exercise-15/complete/shared/Page.js b/exercise-12/complete/shared/Page.js similarity index 100% rename from exercise-15/complete/shared/Page.js rename to exercise-12/complete/shared/Page.js diff --git a/exercise-15/complete/shared/Page.module.css b/exercise-12/complete/shared/Page.module.css similarity index 100% rename from exercise-15/complete/shared/Page.module.css rename to exercise-12/complete/shared/Page.module.css diff --git a/exercise-15/complete/theme/Provider.js b/exercise-12/complete/theme/Provider.js similarity index 100% rename from exercise-15/complete/theme/Provider.js rename to exercise-12/complete/theme/Provider.js diff --git a/exercise-15/complete/theme/Switcher.js b/exercise-12/complete/theme/Switcher.js similarity index 100% rename from exercise-15/complete/theme/Switcher.js rename to exercise-12/complete/theme/Switcher.js diff --git a/exercise-15/complete/theme/Switcher.module.css b/exercise-12/complete/theme/Switcher.module.css similarity index 100% rename from exercise-15/complete/theme/Switcher.module.css rename to exercise-12/complete/theme/Switcher.module.css diff --git a/exercise-15/complete/theme/context.js b/exercise-12/complete/theme/context.js similarity index 100% rename from exercise-15/complete/theme/context.js rename to exercise-12/complete/theme/context.js diff --git a/exercise-15/complete/theme/static.js b/exercise-12/complete/theme/static.js similarity index 100% rename from exercise-15/complete/theme/static.js rename to exercise-12/complete/theme/static.js diff --git a/exercise-15/data/db.json b/exercise-12/data/db.json similarity index 100% rename from exercise-15/data/db.json rename to exercise-12/data/db.json diff --git a/exercise-12/friend-detail/FriendDetail.entry.js b/exercise-12/friend-detail/FriendDetail.entry.js index 9b25248..772ea33 100644 --- a/exercise-12/friend-detail/FriendDetail.entry.js +++ b/exercise-12/friend-detail/FriendDetail.entry.js @@ -1,13 +1,22 @@ import React from 'react'; -import friends from '../data/friends'; - import FriendDetail from './FriendDetail'; +import getFriendFromApi from './get-friend-from-api'; + +export default class FriendDetailEntry extends React.Component { + state = { + friend: undefined, + }; -export default function({match}) { - // the match prop is passed in via react.router - const friendId = match.params.id; - const friend = friends.find(x => x.id === parseInt(friendId, 10)); + async componentDidMount() { + // the match prop is passed in via react.router + const friend = await getFriendFromApi(this.props.match.params.id); + this.setState({ + friend, + }); + } - return ; + render() { + return ; + } } diff --git a/exercise-12/friend-detail/FriendDetail.js b/exercise-12/friend-detail/FriendDetail.js index 8ae18f8..33c3aec 100644 --- a/exercise-12/friend-detail/FriendDetail.js +++ b/exercise-12/friend-detail/FriendDetail.js @@ -6,6 +6,19 @@ import FriendFlipper from './FriendFlipper'; import styles from './FriendDetail.module.css'; +function renderFriend(friend) { + if (friend === undefined) { + return

Loading...

; + } + + return ( +
+

{friend.name}

+ +

{friend.bio}

+
+ ); +} export default function({ friend }) { return ( @@ -13,13 +26,7 @@ export default function({ friend }) {
< Home
- -
-

{friend.name}

- -

{friend.bio}

-
-
+ {renderFriend(friend)}
); diff --git a/exercise-12/friend-detail/FriendFlipper.js b/exercise-12/friend-detail/FriendFlipper.js index 4d0b81e..a904b1f 100644 --- a/exercise-12/friend-detail/FriendFlipper.js +++ b/exercise-12/friend-detail/FriendFlipper.js @@ -1,12 +1,29 @@ import React from 'react'; +import classNames from 'classnames'; +import theme from '../theme/static'; import styles from './FriendFlipper.module.css'; export default class FriendFlipper extends React.Component { + state = { + flipped: false, + }; + + handleFlipped = () => { + this.setState(prevProps => { + return { + flipped: !prevProps.flipped, + }; + }); + }; + render() { return (
-
{this.renderFront()}
+
+ {this.state.flipped ? null : this.renderFront()} + {!this.state.flipped ? null : this.renderBack()} +
); } @@ -16,8 +33,12 @@ export default class FriendFlipper extends React.Component { return (
- {friend.name} -
@@ -29,7 +50,7 @@ export default class FriendFlipper extends React.Component { const { friend } = this.props; return (
-
+
{friend.image}

@@ -43,7 +64,11 @@ export default class FriendFlipper extends React.Component { ))}

-
diff --git a/exercise-12/friend-detail/FriendFlipper.module.css b/exercise-12/friend-detail/FriendFlipper.module.css index da93169..8135352 100644 --- a/exercise-12/friend-detail/FriendFlipper.module.css +++ b/exercise-12/friend-detail/FriendFlipper.module.css @@ -9,6 +9,7 @@ .flipper { position: relative; + background-color: gray; } /* hide back of pane during swap */ @@ -22,14 +23,21 @@ box-shadow: 0 1px 2px; } +/* front pane, placed above back */ + .flipperNav { - background: var(--brand-dark); border: none; padding: 5px 10px; color: white; text-decoration: underline; cursor: pointer; } +.flipperNav.green { + background: teal; +} +.flipperNav.purple { + background: blueviolet; +} .flipperNav:focus { opacity: 0.8; outline: none; @@ -41,12 +49,17 @@ } .backContents { font-size: 0.8rem; - background-color: var(--brand-dark); color: white; text-align: left; height: calc(100% - 10px); padding: 5px; } +.backContents.green { + background: teal; +} +.backContents.purple { + background: blueviolet; +} .backContents .flipperNav { position: absolute; bottom: 5px; diff --git a/exercise-15/friend-detail/get-friend-from-api.js b/exercise-12/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-15/friend-detail/get-friend-from-api.js rename to exercise-12/friend-detail/get-friend-from-api.js diff --git a/exercise-12/friends/Friends.entry.js b/exercise-12/friends/Friends.entry.js index df30e43..0ad1ffa 100644 --- a/exercise-12/friends/Friends.entry.js +++ b/exercise-12/friends/Friends.entry.js @@ -1,10 +1,22 @@ import React from 'react'; -import myFriends from '../data/friends'; +import getFriendsFromApi from './get-friends-from-api'; import Friends from './Friends'; -export default function FriendsEntry() { - return -} +export default class FriendsEntry extends React.Component { + state = { + friends: [] + } + + async componentDidMount() { + const friends = await getFriendsFromApi(); + this.setState({ + friends + }); + } + render() { + return ; + } +} diff --git a/exercise-15/friends/get-friends-from-api.js b/exercise-12/friends/get-friends-from-api.js similarity index 100% rename from exercise-15/friends/get-friends-from-api.js rename to exercise-12/friends/get-friends-from-api.js diff --git a/exercise-12/shared/Card.js b/exercise-12/shared/Card.js index 5268294..b5830c4 100644 --- a/exercise-12/shared/Card.js +++ b/exercise-12/shared/Card.js @@ -1,7 +1,11 @@ import React from 'react'; +import classNames from 'classnames'; +import theme from '../theme/static'; import styles from './Card.module.css'; export default function Card({ children }) { - return
{children}
; + return ( +
{children}
+ ); } diff --git a/exercise-12/shared/Card.module.css b/exercise-12/shared/Card.module.css index 3aa7162..7be984d 100644 --- a/exercise-12/shared/Card.module.css +++ b/exercise-12/shared/Card.module.css @@ -1,8 +1,13 @@ .card { min-width: 150px; - background-color: var(--brand-light); margin: 20px; padding: 30px; - box-shadow: 0 1px 2px var(--brand-dark); } - +.card.green { + background-color: mintcream; + box-shadow: 0 1px 2px teal; +} +.card.purple { + background-color: ghostwhite; + box-shadow: 0 1px 2px purple; +} diff --git a/exercise-12/shared/Page.js b/exercise-12/shared/Page.js index ba4950b..c562e9a 100644 --- a/exercise-12/shared/Page.js +++ b/exercise-12/shared/Page.js @@ -1,10 +1,12 @@ import React from 'react'; +import classNames from 'classnames'; +import theme from '../theme/static'; import styles from './Page.module.css'; export default function Page({ children }) { return ( -
+
{children}
); diff --git a/exercise-12/shared/Page.module.css b/exercise-12/shared/Page.module.css index 0681578..5a58fd6 100644 --- a/exercise-12/shared/Page.module.css +++ b/exercise-12/shared/Page.module.css @@ -2,9 +2,14 @@ display: flex; flex-direction: row; justify-content: center; - background: repeating-linear-gradient(to top, var(--brand-light), var(--brand-dark)); height: calc(100vh - 140px); } +.page.green { + background: repeating-linear-gradient(to top, mintcream, teal); +} +.page.purple { + background: repeating-linear-gradient(to top, ghostwhite, blueviolet); +} .content{ background-color: white; diff --git a/exercise-15/theme/Provider.js b/exercise-12/theme/Provider.js similarity index 100% rename from exercise-15/theme/Provider.js rename to exercise-12/theme/Provider.js diff --git a/exercise-15/theme/Switcher.js b/exercise-12/theme/Switcher.js similarity index 100% rename from exercise-15/theme/Switcher.js rename to exercise-12/theme/Switcher.js diff --git a/exercise-15/theme/Switcher.module.css b/exercise-12/theme/Switcher.module.css similarity index 100% rename from exercise-15/theme/Switcher.module.css rename to exercise-12/theme/Switcher.module.css diff --git a/exercise-15/theme/context.js b/exercise-12/theme/context.js similarity index 100% rename from exercise-15/theme/context.js rename to exercise-12/theme/context.js diff --git a/exercise-15/theme/static.js b/exercise-12/theme/static.js similarity index 100% rename from exercise-15/theme/static.js rename to exercise-12/theme/static.js diff --git a/exercise-13/README.md b/exercise-13/README.md index 0880a76..9512cd0 100644 --- a/exercise-13/README.md +++ b/exercise-13/README.md @@ -1,8 +1,8 @@ # Exercise 13 -## Modern JS: Async/Await +## Modern JavaScript: Class Syntax -This exercise introduces you to the async/await features utilized in modern JavaScript applications. +This exercise introduces you to the class syntax feature utilized in modern JavaScript applications. The instructor will lead you through some code examples. Follow along! @@ -32,4 +32,8 @@ If you do see this output, you're in good shape. The output will change as we mo ### Follow Along -The instructor will lead you in comparing different methods of writing asynchronous unit tests. +The instructor will help you rewrite unit tests using class syntax. + +### Extra Credit + +- Read about [prototypal inheritance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain), and how it differs from classical inheritance. diff --git a/exercise-13/jest.config.js b/exercise-13/jest.config.js index 0619eaf..7571472 100644 --- a/exercise-13/jest.config.js +++ b/exercise-13/jest.config.js @@ -1,4 +1,4 @@ -const exercise = 'exercise-13'; +const exercise = 'exercise-3'; module.exports = { rootDir: '../', diff --git a/exercise-3/things.js b/exercise-13/things.js similarity index 100% rename from exercise-3/things.js rename to exercise-13/things.js diff --git a/exercise-3/things.spec.js b/exercise-13/things.spec.js similarity index 100% rename from exercise-3/things.spec.js rename to exercise-13/things.spec.js diff --git a/exercise-7/App.css b/exercise-14/App.css similarity index 100% rename from exercise-7/App.css rename to exercise-14/App.css diff --git a/exercise-14/App.js b/exercise-14/App.js index 5ef2e8b..9f63621 100644 --- a/exercise-14/App.js +++ b/exercise-14/App.js @@ -1,26 +1,18 @@ import React from 'react'; - -import { BrowserRouter, Route } from 'react-router-dom'; - -import Friends from './friends/Friends.entry'; -import FriendDetail from './friend-detail/FriendDetail.entry'; - -import styles from './App.module.css'; +import './App.css'; +import Exercise from './Exercise'; function App() { return ( - -
-
-

Exercise 14

-

Loading Data

-
-
- - -
+
+
+

Exercise 7

+

Convert a Component

+
+
+
- +
); } diff --git a/exercise-7/Exercise.js b/exercise-14/Exercise.js similarity index 100% rename from exercise-7/Exercise.js rename to exercise-14/Exercise.js diff --git a/exercise-14/README.md b/exercise-14/README.md index d71cb4d..aa96aef 100644 --- a/exercise-14/README.md +++ b/exercise-14/README.md @@ -1,156 +1,109 @@ # Exercise 14 -## Loading Data +## Convert A Component -This exercise introduces you to the usual method of loading data from an API in a React component. +Components that don't maintain their own state or use React methods other than `render()` can be converted from class syntax components to stateless functional components. This reduces code boilerplate. + +In this exercise, you'll convert a component from a function to a class. This is an activity that, until React version 16.8, happened quite often during development. 👉 Start the app for Exercise 14 In a console window, pointed at the root of this project, run `npm run start-exercise-14`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 14: Loading Data", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. - -### The `friends` API - -Prior to this exercise, we were using a static list of friends, imported from the file `data/friends.js`. We're going to instead retrieve our data from a simple API based on the contents of `data/db.json`. - -The API is already running. To see it in action, you can navigate to an endpoint in your browser. - -👉 Browse to the URL `http://localhost:3000/api/friends`. - -You should see a JSON response that contains our three friends. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 14: Convert a Component". If it doesn't, ask your neighbor for assistance or raise your hand. -If you change any contents in `data/db.json`, the `friends` endpoint will reflect it. (Though you will have to refresh the page to see the updates.) +👉 Open Exercise.js -### Lifecycle Events +All of your work for this exercise will take place in Exercise.js. -Several lifecycle events are involved when loading data from an API: initialization, `render`, and `componentDidMount`. +### Background -#### Initialization +In this exercise, we have two components in `Exercise.js` - `Friends` and `FriendProfile`. Neither of these components needs state, and neither needs any React methods other than `render()`. We can convert them both! -The state to be loaded from an API is initialized to an empty value. +### The Process -For a refresher on how to initialize state, see [exercise 12](../exercise-12/README.md#initializing-state). +When converting to a stateless functional component, there are three things that need to be done: -#### `render` +#### 1. Convert the class to a function of the same name -The state data is rendered in the `render` function of a component. When the component initially loads, it renders with an empty value (`undefined`, or `null`, or an empty array, or whatever you choose). +For example, -After the data is completed loading from the API, the component renders with the updated state. - -#### componentDidMount - -The `componentDidMount` lifecycle event fires right after a React component is added to the DOM. From within `componentDidMount`, we'll call the API endpoint. +```jsx +class FriendProfile extends React.Component { + // render()... +} +``` -When the API call is complete, we can use `setState()` to update the state with the loaded data. +becomes -Recall that to easily handle asynchronous processing in a component lifecycle event, you can simply mark the method with `async`, and call `await` within it. +```jsx +function FriendProfile(props) { + // render()... +} +``` -#### An example +#### 2. Remove the render() method -Following is a simple example of how the lifecycle events work together to load data into a component. +For example, ```jsx -export default class MyComponent extends React.Component { - // Initialization - state = { - items: [], - }; - - // Render the state +function FriendProfile(props) { render() { - return ; - } - - // Load the state from an API - async componentDidMount() { - const items = await loadItemsFromApi(); - this.setState({ - items, - }); + // ...what gets rendered } } ``` -### Loading the FriendsEntry data from the API - -The first component we'll update to pull from the API is the `FriendsEntry` component, located at `friends/Friends.entry.js`. - -#### get-friends-from-api.js - -We've included a function in `friends/get-friends-from-api.js`, which will make the API call to collect all of our friends. It uses the `axios` library to make an HTTP call to the `friends` API endpoint. - -👉 Import the `getFriendsFromApi` function into `friends/Friends.entry.js`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-import-api). - -Before we can use our lifecycle events to connect to the API endpoint, we'll need to convert our component to a stateful one. +becomes -👉 Convert the `FriendsEntry` component from a stateless functional component to a stateful class syntax component. - -For a reminder on how to do this, see [exercise 7](../exercise-7/README.md#the-process). - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-stateful). - -#### Initialize the state - -Our component renders a list of friends. We'll want to initialize our component state so that it contains an empty friends array. - -👉 Initialize the state of the `FriendsEntry` component so that it contains an empty array named `friends`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-initialize). - -#### Render the `friends` data from local state - -👉 Modify the `render()` function of the `FriendsEntry` component so that it renders the friends from `this.state.friends`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-render). - -#### Call the `friends` API to get data - -The final step for connecting the `FriendsEntry` component to an API is to load the data from within `componentDidMount()`. - -👉 Add a `componentDidMount()` method that (a) calls the API to get friend data, then (b) calls `setState()` to update the state of the component with the friend data. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-componentdidmount). +```jsx +function FriendProfile(props) { + // ...what gets rendered +} +``` -### Loading FriendDetailEntry data from the API +#### 3. Convert `this.props` references to `props`. -The `FriendDetailEntry` component, at `friend-detail/FriendDetail.entry.js`, also needs to load data from an API endpoint. +For example, -#### Handling an empty friend +```jsx +{ + this.props.name; +} +``` -In the previous activity, the `FriendsEntry` component worked with an empty array for the default state. In this activity, the `FriendDetailEntry` component will need to account for an `undefined` friend. This situation can happen when the component is still loading data from the API, and if our component can't handle an `undefined` friend, it will err out. +becomes -A great place to handle this dichotomy is within the `FriendDetail` component, in `friends/FriendDetail.js`. +```jsx +{ + props.name; +} +``` -👉 Modify the `FriendDetail` component to render an appropriately constructed page when an undefined `friend` is passed in. +That's all it takes to convert from a class syntax component to a stateless functional one! -If an actual `friend` is passed in, it should continue to render the full `FriendDetail` information. +👉 Convert the `Friends` component to a stateless functional component -If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetail-handle-empty-friend). +Check your browser to make sure the components are still rendering properly! -👉 Repeat the activity of loading data from an API for the `FriendDetailEntry` component. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-to-stateless). -Refer to the notes above as a reminder of how to do this. There are a couple details that make this component different than the first: +👉 Convert the `FriendProfile` component to a stateless functional component -- You'll only be loading one friend this time. -- It should default to `undefined`, instead of an empty array. -- The ID for the current friend will be passed into the `FriendDetailEntry` component via the `match.params.id` prop, thanks to ReactRouter. -- The function that calls the API is in `friend-detail/get-friend-from-api.js`. +Check your browser to make sure the components are still rendering properly! -If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetailentry). +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-to-stateless). -### Test it out +Sometimes, we need to convert the other direction - from stateless functional component to class syntax component. -You should now have your friends loading from API endpoints throughout the app. +👉 Convert the `Friends` component back to a class syntax component -You can verify this by making a change in `data/db.json`, and making sure the change is reflected in the app. You will need to refresh the app to see the change. +Check your browser to make sure the components are still rendering properly! -### Extra Credit +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-to-class). -- Show a "loading" indicator when the friends array has not yet loaded on the Friends list page. +👉 Convert the `FriendProfile` component back to a class syntax component -- Read more about [state & lifecycle events](https://reactjs.org/docs/state-and-lifecycle.html). +Check your browser to make sure the components are still rendering properly! -- Read about another use of React lifecycle events - [integrating with non-React libraries](https://reactjs.org/docs/integrating-with-other-libraries.html). +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-to-class). diff --git a/exercise-14/SOLUTIONS.md b/exercise-14/SOLUTIONS.md index 07c9b04..fcebc55 100644 --- a/exercise-14/SOLUTIONS.md +++ b/exercise-14/SOLUTIONS.md @@ -1,103 +1,51 @@ # Possible Solutions -## Friends: Import API -```js -import getFriendsFromApi from './get-friends-from-api'; -``` +## Friends to stateless -## Friends: Stateful ```jsx -export default class FriendsEntry extends React.Component { - render() { - return - } +export default function Friends(props) { + return myFriends.map(friend => ( + + )); } ``` -## Friends: Initialize +## FriendProfile to stateless ```jsx -export default class FriendsEntry extends React.Component { - state = { - friends: [] - } - - // ... +function FriendProfile(props) { + return ( +
+ {props.name} + {props.age ? ` (${props.age})` : null} +
+ ); } ``` -## Friends: render +## Friends to class + ```jsx -export default class FriendsEntry extends React.Component { - // ... - +export default class Friends extends React.Component { render() { - return ; + return myFriends.map(friend => ( + + )); } } ``` -## Friends: componentDidMount -```jsx -export default class FriendsEntry extends React.Component { - // ... - - async componentDidMount() { - const friends = await getFriendsFromApi(); - this.setState({ - friends - }); - } -} -``` +## FriendProfile to class -## FriendDetail: Handle empty friend ```jsx -export default function({ friend }) { - return ( - -
-
- < Home -
- {renderFriend(friend)} -
-
- ); -} - -function renderFriend(friend) { - if (friend === undefined) { - return

Loading...

; - } - - return ( -
-

{friend.name}

- -

{friend.bio}

-
- ); -} -``` - -## FriendDetailEntry -```jsx -export default class FriendDetailEntry extends React.Component { - state = { - friend: undefined, - }; - +class FriendProfile extends React.Component { render() { - return ; - } - - async componentDidMount() { - // the match prop is passed in via react.router - const friend = await getFriendFromApi(this.props.match.params.id); - this.setState({ - friend, - }); + return ( +
+ {this.props.name} + {this.props.age ? ` (${this.props.age})` : null} +
+ ); } } -``` +``` \ No newline at end of file diff --git a/exercise-7/complete/Exercise.js b/exercise-14/complete/Exercise.js similarity index 100% rename from exercise-7/complete/Exercise.js rename to exercise-14/complete/Exercise.js diff --git a/exercise-14/complete/friend-detail/FriendDetail.entry.js b/exercise-14/complete/friend-detail/FriendDetail.entry.js deleted file mode 100644 index c90c4e1..0000000 --- a/exercise-14/complete/friend-detail/FriendDetail.entry.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import FriendDetailFinished from './FriendDetail'; -import getFriendFromApi from './get-friend-from-api'; - -export default class FriendDetailEntry extends React.Component { - state = { - friend: undefined, - }; - - async componentDidMount() { - // the match prop is passed in via react.router - const friend = await getFriendFromApi(this.props.match.params.id); - this.setState({ - friend, - }); - } - - render() { - return ; - } -} diff --git a/exercise-14/complete/friend-detail/FriendDetail.js b/exercise-14/complete/friend-detail/FriendDetail.js deleted file mode 100644 index 11d1636..0000000 --- a/exercise-14/complete/friend-detail/FriendDetail.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import Page from '../shared/Page'; -import Card from '../shared/Card'; -import FriendFlipper from './FriendFlipper'; - -import styles from './FriendDetail.module.css'; - -export default function({ friend }) { - return ( - -
-
- < Home -
- {renderFriend(friend)} -
-
- ); -} - -function renderFriend(friend) { - if (friend === undefined) { - return

Loading...

; - } - - return ( -
-

{friend.name}

- -

{friend.bio}

-
- ); -} diff --git a/exercise-15/App.js b/exercise-15/App.js deleted file mode 100644 index 49c74a4..0000000 --- a/exercise-15/App.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { BrowserRouter, Route } from 'react-router-dom'; - -import Header from './Header'; - -import Friends from './friends/Friends.entry'; -import FriendDetail from './friend-detail/FriendDetail.entry'; - -import styles from './App.module.css'; - -function App() { - return ( - -
-
-
- - -
-
-
- ); -} - -export default App; diff --git a/exercise-15/README.md b/exercise-15/README.md index e475d1f..72aec93 100644 --- a/exercise-15/README.md +++ b/exercise-15/README.md @@ -1,244 +1,5 @@ # Exercise 15 -## React Context +## Legacy State Management -In this exercise, we'll use React Context to manage application-level state. That is, state that applies to the entire application, instead of a single component (or a few closely-related components). - -Throughout the workshop, you've seen the app alternate between green and purple themes. We're going to automate that, with a "theme switcher" button. - -👉 Start the app for Exercise 15 - -In a console window, pointed at the root of this project, run `npm run start-exercise-15`. - -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 15: React Context", and our three adorable kitten friends in the purple theme. If it doesn't, ask your neighbor for assistance or raise your hand. - -### Static theme - -We've modified the CSS Modules coming into this exercise, so that they render based on a static theme. The static theme is defined in `./theme/static/index.js`. If you change the static theme, the UI will reflect it. - -👉 Change the exported value in `./theme/static/index.js` from `purple` to `green`. - -In your browser, you should see the theme change from purple to green. - -### ThemeSwitcher - -In `Header.js`, you'll see that we've rendered a new component named ``. This shows up in the UI as a button that says "Change Theme". We're going to hook theme-toggling up to this `ThemeSwitcher` component, using React Context. - -### React Context - -The React Context API involves three players: a Context, a Provider, and many Consumers. - -The Context defines that we will use a specific type of context in our app. - -### ThemeContext - -For this exercise, we've already created the Context. It is named `ThemeContext`, and is located at `theme/context.js`. - -👉 Open the `theme/context.js` file. - -You should see this: - -```js -import React from 'react'; - -export default React.createContext(); -``` - -There isn't much happening here. The important thing is that it is calling `React.createContext()`, and exporting the result. When we want to create a Provider or Consumer, we'll need to use this Context. - -### ThemeProvider - -A Provider handles the state management for a Context. - -For this exercise, we've already created the Provider. It is named `ThemeProvider`, and it's located at `theme/Provider.js`. - -👉 Open the `theme/Provider.js` file. - -You should see a component that looks similar to other stateful components. It includes several pieces we've seen in previous exercises. - -#### State Initialization - -The `ThemeProvider` initializes its state, so that the `theme` is defaulted to `purple`. - -```jsx -export default class ThemeProvider extends React.Component { - state = { - theme: 'purple', - }; - - // ... -} -``` - -#### Handle State Change - -The `ThemeProvider` includes a `handleTimeChange()` method, which updates the state of the Context using `setState`. - -This handler is swapping the value of `this.state.theme` between `green` and `purple` each time it is called. - -```jsx -export default class ThemeProvider extends React.Component { - // ... - - handleThemeChange = () => { - this.setState(prevState => ({ - theme: prevState.theme === 'green' ? 'purple' : 'green', - })); - }; - - // ... -} -``` - -#### render() - -The last thing the `ThemeProvider` does is render the current state. - -A Context Provider passes the state down to its children via the `value` prop. The `value` prop can contain a single value, or it can contain an object if there are multiple values to pass down. - -In this case, we need to pass down the current theme, as well as our handler that toggles the theme from `green` to `purple`. Thus we create an object (`data`), and pass that into the rendered ``, instead of a single value. - -```jsx -export default class ThemeProvider extends React.Component { - // ... - - render() { - const data = { - theme: this.state.theme, - onThemeChanged: this.handleThemeChange, - }; - - return ( - - {this.props.children} - - ); - } -} -``` - -Inside the `` is rendered the children passed into the provider. This allows the Provider to wrap a component tree, and pass the state down to all of its children. - -### Wrap the app in a `` - -Having created a `ThemeProvider`, we need to wrap our component tree within it, so that it may pass the state throughout the app. - -👉 Wrap the app in a `` component, within `App.js`. - -We want our entire app to have access to the ThemeContext, so we'll wrap the entire app in the `ThemeProvider`. - -You'll want to import the `ThemeProvider`, and wrap the rendered `App` component within a `` element. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#app-themeprovider). - -### Adding ThemeContext.Consumers - -Now that we've wrapped the app in the `ThemeProvider`, we can attach Consumers anywhere down the tree. - -Consumers look like this: - -```jsx -import ThemeContext from './theme/context'; - -export default function MyComponent() { - return ( - - {value => } - - ); -} -``` - -The child of the `` is a function. It takes an argument that contains the `value` that was passed down by the `ThemeContext.Provider`. - -Inside the inner function, your components can do whatever they need with the value passed in. - -Remember that in our case, we have two properties on the `value` that we are interested in: the `theme` and an `onThemeChanged` handler. This means we could use object destructuring to write a consumer like this: - -```jsx -import ThemeContext from './theme/context'; - -export default function MyComponent() { - return ( - - {({theme, onThemeChanged}) => ( - - }} - - ) -} -``` - -### Make the Switcher a Consumer - -The `Switcher` component is located at `theme/Switcher.js`. It is the button that toggles the theme from `green` to `purple`. In the context of our ThemeContext, that means it is a Consumer that needs to call the `onThemeChanged` handler. - -👉 Wrap the `Switcher` component in `` - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#switcher-themecontext.consumer). - -Wrapping in `` gets us access to the `value` passed down from the `ThemeContext.Provider` - which includes an `onThemeChanged` handler. We need to connect our button to that handler. - -👉 Within the `Switcher` component, connect the button `click` event to the `onThemeChanged` handler passed into the rendering function. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#switcher-onthemechanged). - -### Make the Header a Consumer - -We've hooked up the Consumer that modifies the application-level state; now we need to connect the Consumers that read the application-level state. This starts with the `Header` component, located at `Header.js`. - -👉 Wrap the `Header` component's `
` element in a `` component. - -Wrapping in `` gets us access to the `value` passed down from the `ThemeContext.Provider` - which includes a `theme` property. We need to connect our header to that property. - -Now that we're using the `theme` passed into the rendering function, we no longer need the static theme imported from `./theme/static`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#header-consumer). - -### Test it out - -At this point you should see the header color change when you click the button. - -If you don't, re-read the instructions above thoroughly. If you can't figure out what you missed, ask your neighbor, or raise your hand. - -### Make the `Page` shared component a `ThemeContext.Consumer` - -The `Page` component, located at `shared/Page.js`, needs to also utilize the `theme` property from the ThemeContext. - -👉 Modify the `Page` component to be a ``, utilizing the `theme` property in its rendering function. - -Reference the [Header](#make-the-header-a-consumer) instructions if you can't remember how to make this happen. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#page-consumer). - -### Make the `Card` shared component a `ThemeContext.Consumer` - -The `Card` component, located at `shared/Card.js`, needs to also utilize the `theme` property from the ThemeContext. - -👉 Modify the `Card` component to be a ``, utilizing the `theme` property in its rendering function. - -Reference the [Header](#make-the-header-a-consumer) instructions if you can't remember how to make this happen. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#card-consumer). - -### Make the `FriendFlipper` a `ThemeContext.Consumer` - -The `FriendFlipper` component, located at `friend-detail/FriendFlipper.js`, needs to also utilize the `theme` property from the ThemeContext. - -👉 Modify the `FriendFlipper` component to be a ``, utilizing the `theme` property in its rendering function. - -Reference the [Header](#make-the-header-a-consumer) instructions if you can't remember how to make this happen. - -In this component, there is a `renderFront()` and `renderBack()` method. Both of these could individually be made Consumers, or you could make the overall `render()` method a Consumer and pass the `theme` into `renderFront()` and `renderBack()`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendflipper-consumer). - -### Test it out - -At this point your entire app should change from green to purple and vice versa when the "Change Theme" button is clicked. - -If this doesn't happen, re-read the instructions above thoroughly. If you can't figure out what you missed, ask your neighbor, or raise your hand. - -### Extra Credit - -[Read about React Context vs Redux](https://daveceddia.com/context-api-vs-redux/). +TODO: write me diff --git a/exercise-15/SOLUTIONS.md b/exercise-15/SOLUTIONS.md deleted file mode 100644 index 084e50e..0000000 --- a/exercise-15/SOLUTIONS.md +++ /dev/null @@ -1,141 +0,0 @@ -# Possible Solutions - -## App: ThemeProvider - -```jsx -import ThemeProvider from './theme/Provider'; - -function App() { - return ( - - -
- // ... -
-
-
- ); -} - -``` - -## Switcher: ThemeContext.Consumer - -```jsx -import ThemeContext from './context'; - -export default function() { - return ( - - {({ theme }) => ( - - )} - - ); -} -``` - -## Switcher: onThemeChanged -```jsx -import ThemeContext from './context'; - -export default function() { - return ( - - {({ theme, onThemeChanged }) => ( - - )} - - ); -} -``` - -## Header: Consumer -```jsx -// import theme from './theme/static'; // no longer needed -import ThemeContext from './theme/context'; - -export default function Header() { - return ( - - {({ theme }) => ( -
- ... -
- ); -} -``` - -## Page: Consumer -```jsx -import ThemeContext from '../theme/context'; - -export default function Page({ children }) { - return ( - - {({ theme }) => ( -
-
{children}
-
- )} -
- ); -} -``` - -## Card: Consumer -```jsx -import ThemeContext from '../theme/context'; - -export default function Card({ children }) { - return ( - - {({theme}) => ( -
{children}
- )} -
- ); -} -``` - -## FriendFlipper: Consumer -```jsx -import ThemeContext from '../theme/context'; - -export default class FriendFlipper extends React.Component { - - // ... - - renderFront() { - const { friend } = this.props; - return ( - - {({ theme }) => ( -
- // ... -
- )} -
- ); - } - - renderBack() { - const { friend } = this.props; - return ( - - {({ theme }) => ( -
- // ... -
- )} -
- ); - } -} -``` diff --git a/exercise-15/complete/friend-detail/FriendFlipper.js b/exercise-15/complete/friend-detail/FriendFlipper.js deleted file mode 100644 index ecc54a7..0000000 --- a/exercise-15/complete/friend-detail/FriendFlipper.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import ThemeContext from '../theme/context'; - -import styles from './FriendFlipper.module.css'; - -export default class FriendFlipper extends React.Component { - state = { - flipped: false, - }; - - handleFlipped = () => { - this.setState(prevProps => { - return { - flipped: !prevProps.flipped, - }; - }); - }; - - render() { - return ( -
-
- {this.state.flipped ? null : this.renderFront()} - {!this.state.flipped ? null : this.renderBack()} -
-
- ); - } - - renderFront() { - const { friend } = this.props; - return ( - - {({ theme }) => ( -
-
- {friend.image} - -
-
- )} -
- ); - } - - renderBack() { - const { friend } = this.props; - return ( - - {({ theme }) => ( -
-
- {friend.image} -
-

- ID: - {friend.id} -

-

Colors:

-
    - {friend.colors.map(color => ( -
  • {color}
  • - ))} -
-
- -
-
- )} -
- ); - } -} diff --git a/exercise-15/friends/Friends.entry.js b/exercise-15/friends/Friends.entry.js deleted file mode 100644 index 0ad1ffa..0000000 --- a/exercise-15/friends/Friends.entry.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import getFriendsFromApi from './get-friends-from-api'; - -import Friends from './Friends'; - -export default class FriendsEntry extends React.Component { - state = { - friends: [] - } - - async componentDidMount() { - const friends = await getFriendsFromApi(); - this.setState({ - friends - }); - } - - render() { - return ; - } -} diff --git a/exercise-15/shared/Card.js b/exercise-15/shared/Card.js deleted file mode 100644 index b5830c4..0000000 --- a/exercise-15/shared/Card.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import theme from '../theme/static'; - -import styles from './Card.module.css'; - -export default function Card({ children }) { - return ( -
{children}
- ); -} diff --git a/exercise-16/README.md b/exercise-16/README.md index c2d3dff..7bada2b 100644 --- a/exercise-16/README.md +++ b/exercise-16/README.md @@ -1,232 +1,5 @@ # Exercise 16 -## React Hooks +## Legacy Lifecycle Events -This exercise will give you hands-on experience converting code to use the upcoming React hooks API. Hooks will simplify how we use several React features that we've already seen. - -👉 Start the app for Exercise 16 - -In a console window, pointed at the root of this project, run `npm run start-exercise-16`. - -This will open a browser window pointed at localhost:3000, showing a web app titled "Exercise 16: Hooks", our three adorable kitten friends, and a theme switcher that toggles between purple and green themes. If it doesn't, ask your neighbor for assistance or raise your hand. - -### New Components: FriendFlipperBack and FriendFlipperFront - -We've refactored the FriendFlipper component, extracting the `renderBack()` and `renderFront()` functions into `FriendFlipperBack` and `FriendFlipperFront` components. This will help us focus on the changes we're making. - ---- - -### Rethinking State Management: `useState()` - -The `useState` hook is a way to manage state in a functional React component without converting it to a class. - -Here's an example of a stateful component written as a class (**not** using the `useState` hook): - -```jsx -import React from 'react'; - -class MyCheckBox extends React.Component { - state = { - checked: false, - }; - - handleChanged = e => { - this.setState({ checked: e.target.checked }); - }; - - render() { - return ( - - ); - } -} -``` - -And here is that same component written as a stateful function, with the `useState` hook: - -```jsx -import React, { useState } from 'react'; - -function MyCheckBox() { - const [checked, setChecked] = useState(false); - - return ( - setChecked(!checked)} - /> - ); -} -``` - -There are 4 steps to using the `useState` hook in a component: - -1. If the component is a class, convert it to a function. We practiced this in [exercise 7](../exercise-7/README.md). - -2. Import the `useState` function as a named import, from the 'react' dependency. (`import React, { useState } from 'react';`) - -3. Call `useState` at the beginning of the function. - - a. Pass the default value as the lone argument to `useState`. In the example above, the default value is `false`. - - b. An array is returned by `useState`. The first item in the array represents the current value of the state item; the second item is a function we can call to change the value of the state item. In the example above, we are destructuring the returned array into `checked` and `setChecked` variables. - -4. Use the state variables from 3b (`checked` and `setChecked`) in the rendered component. - -👉 Convert `friend-detail/FriendFlipper.js` from a stateful class component to a stateful function component, using the `useState` hook. - -Use the example & steps above as a guide. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendflipper-usestate). - ---- - -### Rethinking Lifecycle Events: `useEffect()` - -The `useEffect` hook allows you to tap into React lifecycle events in a functional React component, without converting it to a class. - -Here's an example of a class component that utilizes lifecycle events (**not** using the `useEffect` hook): - -```jsx -import React from 'react'; - -class Chat extends React.Component { - componentDidMount () { - socket.emit('join', { id: this.props.friendId }); - } - - componentDidUpdate(nextProps) { - if (nextProps.friendId !== this.props.friendId) { - socket.emit('leave', { id: this.props.friendId }); - socket.emit('join', { id: nextProps.friendId }); - } - } - - componentWillUnmount () { - socket.emit('leave', { id: this.props.friendId }); - } - - render() { ... } -} -``` - -This component joins a socket when the component mounts, leaves the original socket and joins a new one when the `friendId` prop changes, and leaves the socket when the component unmounts. - -Here is the same component written as a functional component that uses the `useEffect` hook: - -```jsx -import React, { useEffect } from 'react'; - -function Chat(props) { - - useEffect(() => { - socket.emit('join', { id: props.friendId }); - - return () => { - socket.emit('leave', { id: props.friendId }); - } - - }, [ props.friendId ]) - - return ( ... ) -} -``` - -There are 3 steps to using the `useEffect` hook in a component: - -1. If the component is a class, convert it to a function. We practiced this in [exercise 7](../exercise-7/README.md). - -2. Import the `useEffect` function as a named import, from the 'react' dependency. (`import React, { useEffect } from 'react';`) - -3. Call `useEffect` at the beginning of the function. - - a. The first argument to `useEffect` is a function that should execute when the component mounts, or when props change that require the effect to re-run. In the example above, this function joins a socket based on `props.friendId`. - - b. Not all effects require "cleanup" code - but if they do, this is accomplished by the function in 3a returning another anonymous function. This returned function will execute when the component unmounts, or when props change that require the effect to re-run. In our example above, this "cleanup" function will leave a socket based on the friendId. - - c. The second argument to `useEffect` is an array. This array will contain all props which, when their value changes, would require the effect to re-run. In the example above, we pass `[ props.friendId ]` - this means that the effect will re-run whenever the value of the `friendId` prop changes. - -👉 Convert `friends/Friends.entry.js` from a class component to a function component, using the `useEffect` hook. - -Use the example & steps above as a guide. - -Notice that this component manages state, in addition to using the `componentDidMount` lifecycle method. You'll want to convert the stateful portions to use `useState`. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends.entry-useeffect). - ---- - -### Rethinking Context: `useContext()` - -The `useContext` hook eliminates the need for the "function as a child" pattern when consuming a context in a component. "Function as a child", similar to the "render props" pattern, is a useful pattern in React apps, but it can also be confusing. You can read more about these patterns [here](https://reactjs.org/docs/render-props.html). - -Here's an example of a component that consumes a context without the `useContext` hook. Notice how the child of `` is a function: - -```jsx -import React from 'react'; -import CurrentUserContext from './context'; - -export default function CurrentUser() { - return ( - - {({ currentUser, handleUserLogout }) => ( -
- {currentUser.name} - -
- )} -
- ); -} -``` - -And here is the same component, using the `useContext` hook: - -```jsx -import React, { useContext } from 'react'; - -import CurrentUserContext from './context'; - -export default function CurrentUser() { - const { currentUser, handleUserLogout } = useContext(CurrentUserContext); - - return ( -
- {currentUser.name} - -
- ); -} -``` - -There are 3 steps to using the `useContext` hook in a component: - -1. Import the `useContext` function as a named import, from the 'react' dependency. (`import React, { useContext } from 'react';`) - -2. Call `useContext` at the beginning of the function. - - a. Pass the associated context as the lone argument to `useContext`. In the example above, that's the `CurrentUserContext` context. - - b. The "value" of the context is returned by `useContext`. This will have been defined by you when you create the context. In our example, it is an object containing `currentUser` and `handleUserLogout` properties. - -3. Use the context value from 2b (an object with `currentUser` and `handleUserLogout`) in the rendered component. - -👉 Convert `theme/Switcher.js` to use the `useContext` hook. - -Use the example & steps above as a guide. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#switcher-usecontext). - -### Extra Credit - -The following components could also be converted to use hooks: - -- friend-detail/FriendDetail.entry.js (`useState`, `useEffect`) -- friend-detail/FriendFlipperBack.js (`useContext`) -- friend-detail/FriendFlipperFront.js (`useContext`) -- theme/Provider.js (`useState`) +TODO: write this diff --git a/exercise-16/complete/App.module.css b/exercise-16/complete/App.module.css deleted file mode 100644 index dbbd108..0000000 --- a/exercise-16/complete/App.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.app { - text-align: center; -} - -.emphasize { - text-decoration: underline; -} diff --git a/exercise-16/complete/friend-detail/FriendDetail.module.css b/exercise-16/complete/friend-detail/FriendDetail.module.css deleted file mode 100644 index 1eac493..0000000 --- a/exercise-16/complete/friend-detail/FriendDetail.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.friendDetail { - width: 50%; -} - -.cardContents { - display: flex; - flex-direction: column; - align-items: center; -} - -.toolbar { - margin: 1em 0; -} \ No newline at end of file diff --git a/exercise-16/friend-detail/FriendDetail.entry.js b/exercise-16/friend-detail/FriendDetail.entry.js deleted file mode 100644 index 772ea33..0000000 --- a/exercise-16/friend-detail/FriendDetail.entry.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import FriendDetail from './FriendDetail'; -import getFriendFromApi from './get-friend-from-api'; - -export default class FriendDetailEntry extends React.Component { - state = { - friend: undefined, - }; - - async componentDidMount() { - // the match prop is passed in via react.router - const friend = await getFriendFromApi(this.props.match.params.id); - this.setState({ - friend, - }); - } - - render() { - return ; - } -} diff --git a/exercise-16/friend-detail/FriendDetail.module.css b/exercise-16/friend-detail/FriendDetail.module.css deleted file mode 100644 index 1eac493..0000000 --- a/exercise-16/friend-detail/FriendDetail.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.friendDetail { - width: 50%; -} - -.cardContents { - display: flex; - flex-direction: column; - align-items: center; -} - -.toolbar { - margin: 1em 0; -} \ No newline at end of file diff --git a/exercise-16/friends/FriendProfile.js b/exercise-16/friends/FriendProfile.js deleted file mode 100644 index c8c886b..0000000 --- a/exercise-16/friends/FriendProfile.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -import Card from '../shared/Card'; - -import styles from './FriendProfile.module.css'; - -export default function FriendProfile({ id, name, image }) { - return ( - - -
- {name} -

{name}

-
-
- - ); -} diff --git a/exercise-16/friends/Friends.entry.js b/exercise-16/friends/Friends.entry.js deleted file mode 100644 index 0ad1ffa..0000000 --- a/exercise-16/friends/Friends.entry.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import getFriendsFromApi from './get-friends-from-api'; - -import Friends from './Friends'; - -export default class FriendsEntry extends React.Component { - state = { - friends: [] - } - - async componentDidMount() { - const friends = await getFriendsFromApi(); - this.setState({ - friends - }); - } - - render() { - return ; - } -} diff --git a/exercise-16/shared/Card.module.css b/exercise-16/shared/Card.module.css deleted file mode 100644 index 7be984d..0000000 --- a/exercise-16/shared/Card.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.card { - min-width: 150px; - margin: 20px; - padding: 30px; -} -.card.green { - background-color: mintcream; - box-shadow: 0 1px 2px teal; -} -.card.purple { - background-color: ghostwhite; - box-shadow: 0 1px 2px purple; -} diff --git a/exercise-2/README.md b/exercise-2/README.md new file mode 100644 index 0000000..862c8b6 --- /dev/null +++ b/exercise-2/README.md @@ -0,0 +1,5 @@ +# Exercise 2 + +## Thinking In Components + +TODO: write me diff --git a/exercise-3/README.md b/exercise-3/README.md index 7ada6d1..0543b76 100644 --- a/exercise-3/README.md +++ b/exercise-3/README.md @@ -1,13 +1,14 @@ # Exercise 3 -## Modern JavaScript: Class Syntax -This exercise introduces you to the class syntax feature utilized in modern JavaScript applications. +## Modern JavaScript: Working With Variables + +This exercise introduces you to many features utilized in modern JavaScript applications for working with variables. The instructor will lead you through some code examples. Follow along! ### Setup -This exercise utilizes unit tests for demonstration purposes. You don't need to know much about unit tests coming in. +This exercise utilizes unit tests for demonstration purposes. You don't need to know much about unit tests coming in. 👉 Start your test suite. Open a new command window at the root of this project, and enter `npm run test-exercise-3`. @@ -31,8 +32,8 @@ If you do see this output, you're in good shape. The output will change as we mo ### Follow Along -The instructor will help you rewrite unit tests using class syntax. +The instructor will help you rewrite unit tests using many modern JavaScript features. ### Extra Credit -* Read about [prototypal inheritance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain), and how it differs from classical inheritance. +Read about [all the helpful methods available for working with arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Methods_2) diff --git a/exercise-4/greetings.js b/exercise-3/greetings.js similarity index 100% rename from exercise-4/greetings.js rename to exercise-3/greetings.js diff --git a/exercise-4/greetings.spec.js b/exercise-3/greetings.spec.js similarity index 100% rename from exercise-4/greetings.spec.js rename to exercise-3/greetings.spec.js diff --git a/exercise-4/App.css b/exercise-4/App.css new file mode 100644 index 0000000..f7c3cab --- /dev/null +++ b/exercise-4/App.css @@ -0,0 +1,40 @@ +.App { + text-align: center; +} + +.App-header { + background-color: blueviolet; + height: 100px; + padding: 20px; + color: white; +} + +.App-title { + font-family: 'Pacifico', cursive; + line-height: 0.5em; +} + +.friends-title { + color: teal; +} + +.friends-greeting-orange { + color: orange; +} + +.friends-greeting-purple { + color: blueviolet; +} + +ul { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; +} +li { + background-color: lavender; + margin: 10px; + padding: 5px; +} diff --git a/exercise-4/App.js b/exercise-4/App.js new file mode 100644 index 0000000..ddb9386 --- /dev/null +++ b/exercise-4/App.js @@ -0,0 +1,17 @@ +import React from 'react'; +import './App.css'; +import Exercise from './Exercise'; + +function App() { + return ( +
+
+

Exercise 4

+

JSX Fundamentals

+
+ +
+ ); +} + +export default App; diff --git a/exercise-4/Exercise.js b/exercise-4/Exercise.js new file mode 100644 index 0000000..0bf10fd --- /dev/null +++ b/exercise-4/Exercise.js @@ -0,0 +1,35 @@ +import React from 'react'; + +const greeting = 'How are you?'; + +function emphasize(text) { + return `${text}!!!!!`; +} + +function determineGreetingClass() { + if (new Date() % 2 === 1) { + return 'friends-greeting-orange'; + } + return 'friends-greeting-purple'; +} + +const myFriends = [ + { + id: 1, + name: 'Potatoes', + age: '4 months', + }, + { + id: 2, + name: 'Flower', + age: '6 months', + }, + { + id: 3, + name: 'Turtle', + }, +]; + +export default function Friends() { + return
; +} diff --git a/exercise-4/README.md b/exercise-4/README.md index e7c6957..95615aa 100644 --- a/exercise-4/README.md +++ b/exercise-4/README.md @@ -1,38 +1,264 @@ # Exercise 4 -## Modern JavaScript: Working With Objects -This exercise introduces you to many features utilized in modern JavaScript applications for working with objects. +## JSX Fundamentals -The instructor will lead you through some code examples. Follow along! +This exercise will introduce you to JSX syntax. JSX is a powerful hybrid of JavaScript and XML, used most often in React components. -### Setup +You'll use JSX to render different types of things to the browser. -This exercise utilizes unit tests for demonstration purposes. You don't need to know much about unit tests coming in. +👉 Start the app for Exercise 4 -👉 Start your test suite. Open a new command window at the root of this project, and enter `npm run test-exercise-3`. +In a console window, pointed at the root of this project, run `npm run start-exercise-4`. -You should see the following output: +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 4: JSX Fundamentals". If it doesn't, ask your neighbor for assistance or raise your hand. +👉 Open Exercise.js + +All of your work for this exercise will take place in Exercise.js. + +### Static HTML + +JSX can be used to render static HTML. Currently, Exercise.js is rendering a single `div` element. + +👉 Modify the `Friends` function component to render an `h1` element as a child of the `div`. + +Inside the h1, place some static text, like "Hello, Friends!". + +Check your browser to see if you succeeded! You should see your static html rendered. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#static-html). + +### Evaluating JavaScript Expressions + +JSX is much more powerful than simple static HTML. We can tell our JSX to evaluate any JavaScript expression with curly braces (`{}`). + +Curly braces can be placed inside any element of your JSX. Any JavaScript expression that can be evaluated can be placed inside the braces. + +For example, this JSX... + +```jsx +
{2 + 1}
``` -No tests found related to files changed since last commit. -Press `a` to run all tests, or run Jest with `--watchAll`. -Watch Usage - › Press a to run all tests. - › Press p to filter by a filename regex pattern. - › Press t to filter by a test name regex pattern. - › Press q to quit watch mode. - › Press Enter to trigger a test run. +would evaluate `2+1`, and render the following element to the browser: + +```html +
3
``` -If you don't see this output, try to investigate the message you see, ask your neighbor, or raise your hand for assistance. +You won't evaluate many expressions like that while building a React app. Here are some you're more likely to see: + +#### Evaluating a variable + +The value of any variable can be rendered in your component. + +For example, if we had a variable declared as `const total = 3` in our component, this JSX... + +```jsx +
{total}
+``` + +would render this element to the browser: + +```html +
3
+``` + +Currently, `Friends.js` has a variable defined named `greeting`. + +👉 Add an `h2` element that displays the value of `greeting` in your `Friends` component. + +Check your browser to see if you succeeded! You should see your greeting rendered. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-a-variable). + +#### Evaluating a function + +A function can be executed from inside our JSX, by calling it within the curly braces. + +For example, the following JSX: + +```jsx +
{add(1, 2)}
+``` + +would execute `add(1, 2)`, and render the result to the browser as the following element: + +```html +
3
+``` + +`Friends.js` contains a function named `emphasize`. It takes a string, and returns the string emphasized with exclamation points. + +👉 Modify the `h2` element in your `Friends` component to call `emphasize` on the `greeting` variable. + +Check your browser to see if you succeeded! You should see your greeting emphasized. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-a-function). + +### Attributes + +Attributes are attached to elements in JSX the same way they are in HTML or XML. For example, this JSX... + +```jsx +
Hello, Friends!
+``` + +will render this element: + +```html +
Hello, Friends!
+``` + +There are a couple of sneaky things to know about attributes in JSX. + +#### className + +Most attributes you assign to a JSX element translate directly to the corresponding HTML attribute. In the example of, `id` in JSX translated directly to `id` in the browser. + +One important outlier is the `class` attribute. + +To emit a `class="..."` attribute in your element, you need to use the `className` attribute in your JSX. + +Side note: Why do you think this is? Why would `class` be different than any other attribute in JSX? We'll discuss after the exercise. + +👉 Add a CSS class named `friends-title` to the `h1` in your `Friends` component. + +Check your browser to see if you succeeded! You should see your `Hello, Friends` title turn blue-green. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#className). + +#### Evaluating attributes + +Just like with children of elements, we can evaluate JavaScript expressions to determine the values of attributes. We simply use the curly braces, instead of quotes, to signify that the value of the attribute is a JavaScript expression. + +For example, this JSX... + +```jsx +
Potatoes
+``` + +would render this element to the browser: + +```html +
Potatoes
+``` + +`Friends.js` contains a function named `determineGreetingClass`. It takes no arguments, and returns one of two CSS class names, based on the current clock ticks (effectively a random number). + +👉 Add a CSS class to the `h2` greeting element, based on the result of the `determineGreetingClass` function. + +Check your browser to see if you succeeded! You should see your greeting message turn either orange or purple. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-attributes). + +### Rendering arrays + +One of the things you'll do frequently in React development is render an array of items. There are a couple secrets to rendering an array. + +#### Array.map + +Remember that JSX is an extension of JavaScript. Anything we can do in JavaScript, we can also do in JSX. + +To render items in an array, we take advantage of the `Array.map` prototype function, and display something for each item. + +For example, imagine we had an array of integers defined as `const items = [1, 2, 3]`. We want to display an unordered list (`ul`) of the items in this array. + +This JSX... + +```jsx +
    + {items.map(item => { + return
  • {item}
  • ; + })} +
+``` + +would render this element to the browser: + +```html +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
+``` + +For each element in the items array, an `li` element would be rendered; the contents of each resolves to the value of `{item}`. + +`Friends.js` contains an array named `myFriends`. + +👉 Following the `h2` greeting of your `Friends` component, render a list of names, based on the `myFriends` variable. + +Check your browser to see if you succeeded! You should see your friends' names listed below the greeting. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#arraymap). + +Preferences vary in regards to mapping over arrays inline (directly in the JSX), defining variables that contain the mapped arrays (and then referencing them in the returned JSX), or calling a function to map an array (and calling it in the returned JSX). You will develop a preference over time. + +#### key + +You might notice a message in your browser console after the previous task: + +`Warning: Each child in an array or iterator should have a unique "key" prop.` + +When rendering an array of items, React prefers that each item rendered have a unique `key` property. This allows React to optimize how elements are rendered to the DOM. It can use this `key` property to identify which items have changed, and which haven't, and update only the ones that have changed. + +To eliminate this message, and improve the performance of our rendered list, we can specify a `key` prop on our rendered items. + +It's important that our key be something that is specific to the item being rendered - often, an ID is a great fit. + +For example, in our previous example, we could render a key for each item with the following JSX: + +```jsx +
    + {items.map(item => ( +
  • {item}
  • + ))} +
+``` + +👉 Add a `key` prop to the items being rendered in your friends list. Use a property from the `myFriends` items that is semantic & unique to each item. + +Check your browser to see if you succeeded! You should not see any error messages in your browser console. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#array-keys). + +### Conditional rendering + +Components often need to display one thing under one condition, and something different under another condition. + +JSX is based on JavaScript, so we can use simple `if` conditions to render conditional elements. + +For example, this JSX... + +```jsx +if (name === 'Potatoes') { + return
You're my favorite friend!
; +} +return
You're okay, I guess.
; +``` + +would render a div based conditionally on the name. + +This same condition can also be accomplished using a ternary operator: + +```jsx +return ( +
+ {name === 'Potatoes' + ? "You're my favorite friend!" + : "You're okay, I guess."} +
+); +``` -If you do see this output, you're in good shape. The output will change as we modify our code. +> Note: See the `(` after the keyword `return`? When we return a JSX element that takes up more than one line, we wrap it in parentheses. This tells the `return` statement that we are going to give it a return value on the next lines. This is an easy thing to forget! -### Follow Along +Some items in the `myFriends` array have ages; some don't. -The instructor will help you rewrite unit tests using many modern JavaScript features. +👉 Conditionally display the age of each friend. If it exists, display it in parentheses next to the name. If it doesn't, display nothing. -### Extra Credit +Check your browser to see if you succeeded! You should see ages next to some friends, but not others. -Read about [all the helpful methods available for working with arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Methods_2) \ No newline at end of file +If you get stuck, [see a possible solution here](./SOLUTIONS.md#conditional-rendering). diff --git a/exercise-4/SOLUTIONS.md b/exercise-4/SOLUTIONS.md new file mode 100644 index 0000000..08edbd7 --- /dev/null +++ b/exercise-4/SOLUTIONS.md @@ -0,0 +1,124 @@ +# Possible solutions + +## Static HTML + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+
+ ); +} +``` + +## Evaluating a variable + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{greeting}

+
+ ); +} +``` + +## Evaluating a function + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
+ ); +} +``` + +## className + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
+ ); +} +``` + +## Evaluating attributes + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
+ ); +} +``` + +## Array.map + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
    + {myFriends.map(friend => { + return
  • {friend.name}
  • ; + })} +
+
+ ); +} +``` + +## Array keys + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
    + {myFriends.map(friend => { + return
  • {friend.name}
  • ; + })} +
+
+ ); +} +``` + +## Conditional rendering + +```jsx +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
    + {myFriends.map(friend => { + return ( +
  • + {friend.name} + {friend.age ? ` (${friend.age})` : null} +
  • + ); + })} +
+
+ ); +} +``` diff --git a/exercise-4/complete/Exercise.js b/exercise-4/complete/Exercise.js new file mode 100644 index 0000000..9fb9ce4 --- /dev/null +++ b/exercise-4/complete/Exercise.js @@ -0,0 +1,50 @@ +import React from 'react'; + +const greeting = 'How are you?'; + +function emphasize(text) { + return `${text}!!!!!`; +} + +function determineGreetingClass() { + if (new Date() % 2 === 1) { + return 'friends-greeting-orange'; + } + return 'friends-greeting-purple'; +} + +const myFriends = [ + { + id: 1, + name: 'Potatoes', + age: '4 months', + }, + { + id: 2, + name: 'Flower', + age: '6 months', + }, + { + id: 3, + name: 'Turtle', + }, +]; + +export default function Friends() { + return ( +
+

Hello, Friends!

+

{emphasize(greeting)}

+
    + {myFriends.map(friend => { + return ( +
  • + {friend.name} + {friend.age ? ` (${friend.age})` : null} +
  • + ); + })} +
+
+ ); +} diff --git a/exercise-10/index.css b/exercise-4/index.css similarity index 100% rename from exercise-10/index.css rename to exercise-4/index.css diff --git a/exercise-15/index.js b/exercise-4/index.js similarity index 100% rename from exercise-15/index.js rename to exercise-4/index.js diff --git a/exercise-5/App.css b/exercise-5/App.css index f7c3cab..17df7f9 100644 --- a/exercise-5/App.css +++ b/exercise-5/App.css @@ -3,7 +3,7 @@ } .App-header { - background-color: blueviolet; + background-color: teal; height: 100px; padding: 20px; color: white; @@ -14,26 +14,13 @@ line-height: 0.5em; } -.friends-title { - color: teal; -} - -.friends-greeting-orange { - color: orange; -} - -.friends-greeting-purple { - color: blueviolet; -} - -ul { - list-style: none; - padding: 0; +.exercise { display: flex; flex-direction: column; align-items: center; } -li { + +.friend-profile { background-color: lavender; margin: 10px; padding: 5px; diff --git a/exercise-5/App.js b/exercise-5/App.js index 71a9fc4..9742b5c 100644 --- a/exercise-5/App.js +++ b/exercise-5/App.js @@ -6,10 +6,12 @@ function App() { return (
-

Exercise 5

-

JSX Fundamentals

+

Exercise 6

+

What can a component render?

- +
+ +
); } diff --git a/exercise-5/Exercise.js b/exercise-5/Exercise.js index 0bf10fd..dc6253d 100644 --- a/exercise-5/Exercise.js +++ b/exercise-5/Exercise.js @@ -1,16 +1,16 @@ import React from 'react'; -const greeting = 'How are you?'; - -function emphasize(text) { - return `${text}!!!!!`; +export default function Friends() { + return
; } -function determineGreetingClass() { - if (new Date() % 2 === 1) { - return 'friends-greeting-orange'; - } - return 'friends-greeting-purple'; +function FriendProfile(props) { + return ( +
+ {props.name} + {props.age ? ` (${props.age})` : null} +
+ ); } const myFriends = [ @@ -29,7 +29,3 @@ const myFriends = [ name: 'Turtle', }, ]; - -export default function Friends() { - return
; -} diff --git a/exercise-5/README.md b/exercise-5/README.md index 9be7250..bf4c6e6 100644 --- a/exercise-5/README.md +++ b/exercise-5/README.md @@ -1,264 +1,138 @@ # Exercise 5 -## JSX Fundamentals +## What can a component render? -This exercise will introduce you to JSX syntax. JSX is a powerful hybrid of JavaScript and XML, used most often in React components. - -You'll use JSX to render different types of things to the browser. +React function components are limited in what they can return. This exercise will introduce you to the 5 most frequently returned kinds of results. 👉 Start the app for Exercise 5 In a console window, pointed at the root of this project, run `npm run start-exercise-5`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 5: JSX Fundamentals". If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 5: What can a component render?". If it doesn't, ask your neighbor for assistance or raise your hand. 👉 Open Exercise.js All of your work for this exercise will take place in Exercise.js. -### Static HTML - -JSX can be used to render static HTML. Currently, Exercise.js is rendering a single `div` element. - -👉 Modify the `Friends` function component to render an `h1` element as a child of the `div`. - -Inside the h1, place some static text, like "Hello, Friends!". - -Check your browser to see if you succeeded! You should see your static html rendered. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#static-html). - -### Evaluating JavaScript Expressions - -JSX is much more powerful than simple static HTML. We can tell our JSX to evaluate any JavaScript expression with curly braces (`{}`). - -Curly braces can be placed inside any element of your JSX. Any JavaScript expression that can be evaluated can be placed inside the braces. - -For example, this JSX... - -```jsx -
{2 + 1}
-``` - -would evaluate `2+1`, and render the following element to the browser: - -```html -
3
-``` - -You won't evaluate many expressions like that while building a React app. Here are some you're more likely to see: - -#### Evaluating a variable - -The value of any variable can be rendered in your component. - -For example, if we had a variable declared as `const total = 3` in our component, this JSX... - -```jsx -
{total}
-``` - -would render this element to the browser: - -```html -
3
-``` - -Currently, `Friends.js` has a variable defined named `greeting`. - -👉 Add an `h2` element that displays the value of `greeting` in your `Friends` component. - -Check your browser to see if you succeeded! You should see your greeting rendered. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-a-variable). +The return value of a React function component is what gets rendered to the DOM. Throughout this exercise, we'll use the terms "render" and "return" interchangeably. -#### Evaluating a function +### Elements/React Components -A function can be executed from inside our JSX, by calling it within the curly braces. +Most frequently, a component will render either an HTML element, or another React component. -For example, the following JSX: +Currently, the `Friends` component is returning an HTML element - a single `
`. Most of the examples we've seen so far have returned HTML elements. -```jsx -
{add(1, 2)}
-``` - -would execute `add(1, 2)`, and render the result to the browser as the following element: - -```html -
3
-``` - -`Friends.js` contains a function named `emphasize`. It takes a string, and returns the string emphasized with exclamation points. - -👉 Modify the `h2` element in your `Friends` component to call `emphasize` on the `greeting` variable. - -Check your browser to see if you succeeded! You should see your greeting emphasized. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-a-function). - -### Attributes - -Attributes are attached to elements in JSX the same way they are in HTML or XML. For example, this JSX... - -```jsx -
Hello, Friends!
-``` - -will render this element: - -```html -
Hello, Friends!
-``` - -There are a couple of sneaky things to know about attributes in JSX. - -#### className - -Most attributes you assign to a JSX element translate directly to the corresponding HTML attribute. In the example of, `id` in JSX translated directly to `id` in the browser. +There is a second component in `Exercise.js` - the `FriendProfile` component. -One important outlier is the `class` attribute. +👉 Modify the `Friends` component to return a single `` element. Pass the name of the first item in the `myFriends` array as a prop to the ``. -To emit a `class="..."` attribute in your element, you need to use the `className` attribute in your JSX. +Check your browser to see if you succeeded! You should see the name `Potatoes` rendered. -Side note: Why do you think this is? Why would `class` be different than any other attribute in JSX? We'll discuss after the exercise. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#react-components). -👉 Add a CSS class named `friends-title` to the `h1` in your `Friends` component. +👉 "Inspect element" in your browser, and look for the elements rendered by the `` component. -Check your browser to see if you succeeded! You should see your `Hello, Friends` title turn blue-green. +You should see that the component is rendered as a div with text content. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#className). +### Fragments -#### Evaluating attributes +Valid JSX requires a single top-level element (similar to how valid XML requires a single top-level element). To demonstrate this, we're going to break our app. -Just like with children of elements, we can evaluate JavaScript expressions to determine the values of attributes. We simply use the curly braces, instead of quotes, to signify that the value of the attribute is a JavaScript expression. +👉 Modify the `Friends` component to return an `

` element adjacent to the `` element. -For example, this JSX... +You should see an error in your browser, similar to this: -```jsx -
Potatoes
``` - -would render this element to the browser: - -```html -
Potatoes
+./exercise-5/Exercise.js + Line 4: Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...? + + 2 | + 3 | export default function Friends() { +> 4 | return

hello, friends!

; + | ^ + 5 | } + 6 | + 7 | function FriendProfile(props) { ``` -`Friends.js` contains a function named `determineGreetingClass`. It takes no arguments, and returns one of two CSS class names, based on the current clock ticks (effectively a random number). - -👉 Add a CSS class to the `h2` greeting element, based on the result of the `determineGreetingClass` function. +As the error indicates, this is invalid syntax. -Check your browser to see if you succeeded! You should see your greeting message turn either orange or purple. +We can fix this a couple ways. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#evaluating-attributes). +#### Wrapping in a `
` -### Rendering arrays +👉 Modify the `Friends` component so that the `

` and `` elements are wrapped in a `
` element. -One of the things you'll do frequently in React development is render an array of items. There are a couple secrets to rendering an array. +Check your browser to see if you succeeded! You should see your title emitted, along with the name `Potatoes`. -#### Array.map +If you get stuck, [see a possible solution here](./SOLUTIONS.md#wrapping-in-a-div). -Remember that JSX is an extension of JavaScript. Anything we can do in JavaScript, we can also do in JSX. +👉 "Inspect element" in your browser, and find the elements rendered by the `` component. -To render items in an array, we take advantage of the `Array.map` prototype function, and display something for each item. +You'll see that your components are wrapped in a `
` - the one you used to wrap the `

` and `` elements. -For example, imagine we had an array of integers defined as `const items = [1, 2, 3]`. We want to display an unordered list (`ul`) of the items in this array. +This solves the problem...but it adds elements to the DOM that we don't really need. One element might not seem like a big deal, but in a large React app, we can end up with significant DOM pollution from this practice. -This JSX... +React v16 introduced a way to solve this problem - the `Fragment`. -```jsx -
    - {items.map(item => { - return
  • {item}
  • ; - })} -
-``` +#### Wrapping in a `` -would render this element to the browser: +The `Fragment` component is basically an empty wrapper. It allows us to provide a single top-level element when we are rendering multiple child elements, but it does not actually render anything to the DOM. -```html -
    -
  • 1
  • -
  • 2
  • -
  • 3
  • -
-``` +You can access the `Fragment` component on the default `React` import (i.e. `React.Fragment`). -For each element in the items array, an `li` element would be rendered; the contents of each resolves to the value of `{item}`. +👉 Replace the wrapping `
` element in the `Friends` component with a wrapping `` element. -`Friends.js` contains an array named `myFriends`. +Check your browser to see if you succeeded! You should still see all friends listed. -👉 Following the `h2` greeting of your `Friends` component, render a list of names, based on the `myFriends` variable. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#wrapping-in-a-fragment). -Check your browser to see if you succeeded! You should see your friends' names listed below the greeting. +👉 "Inspect element" in your browser, and find the elements rendered by the `` component. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#arraymap). +You should see that there's no longer an unnecessary div wrapping your output. -Preferences vary in regards to mapping over arrays inline (directly in the JSX), defining variables that contain the mapped arrays (and then referencing them in the returned JSX), or calling a function to map an array (and calling it in the returned JSX). You will develop a preference over time. +### Arrays -#### key +Sometimes you want to turn an array of data into an array of elements. This can be accomplished with `Array.map`, as we saw in the JSX Fundamentals exercise. -You might notice a message in your browser console after the previous task: +👉 Modify the `Friends` component to return one `` element for each item in the `myFriends` array. For each item, pass the `name` and `age` as props to the ``. -`Warning: Each child in an array or iterator should have a unique "key" prop.` +Don't forget that React wants you to use a `key` prop when you render an array of items! -When rendering an array of items, React prefers that each item rendered have a unique `key` property. This allows React to optimize how elements are rendered to the DOM. It can use this `key` property to identify which items have changed, and which haven't, and update only the ones that have changed. +Check your browser to see if you succeeded! You should see each of the friends listed. -To eliminate this message, and improve the performance of our rendered list, we can specify a `key` prop on our rendered items. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#arrays). -It's important that our key be something that is specific to the item being rendered - often, an ID is a great fit. +👉 "Inspect element" in your browser, and find the elements rendered by the `` component. -For example, in our previous example, we could render a key for each item with the following JSX: +You should see a `
` for each friend. -```jsx -
    - {items.map(item => ( -
  • {item}
  • - ))} -
-``` +### Strings/Numbers -👉 Add a `key` prop to the items being rendered in your friends list. Use a property from the `myFriends` items that is semantic & unique to each item. +Sometimes you want a component to render nothing more than a string or number. When a React function component returns a string or number, it gets rendered as a text node in the DOM. -Check your browser to see if you succeeded! You should not see any error messages in your browser console. +👉 Modify the `FriendProfile` component to return only the `name` prop. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#array-keys). +Check your browser to see if you succeeded! You should see each of the friends listed, abutted against each other. -### Conditional rendering +If you get stuck, [see a possible solution here](./SOLUTIONS.md#strings-or-numbers). -Components often need to display one thing under one condition, and something different under another condition. +👉 "Inspect element" in your browser, and find the elements rendered by the `` component. -JSX is based on JavaScript, so we can use simple `if` conditions to render conditional elements. +You will see that the friend names are rendered as text nodes, all within the same `
`. -For example, this JSX... +### null -```jsx -if (name === 'Potatoes') { - return
You're my favorite friend!
; -} -return
You're okay, I guess.
; -``` +Sometimes we don't want a component to render anything at all. This is usually true when we are using conditional logic to render different results based on inputs. -would render a div based conditionally on the name. - -This same condition can also be accomplished using a ternary operator: - -```jsx -return ( -
- {name === 'Potatoes' - ? "You're my favorite friend!" - : "You're okay, I guess."} -
-); -``` +When the value `null` is returned from a component, nothing gets rendered to the DOM. -> Note: See the `(` after the keyword `return`? When we return a JSX element that takes up more than one line, we wrap it in parentheses. This tells the `return` statement that we are going to give it a return value on the next lines. This is an easy thing to forget! +👉 Modify the `FriendProfile` component to return `null` if the `age` prop is undefined; otherwise return the value of the `name` prop. -Some items in the `myFriends` array have ages; some don't. +Check your browser to see if you succeeded! You should see two of the friends listed - Potatoes and Flower. -👉 Conditionally display the age of each friend. If it exists, display it in parentheses next to the name. If it doesn't, display nothing. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#null). -Check your browser to see if you succeeded! You should see ages next to some friends, but not others. +### Extra Credit -If you get stuck, [see a possible solution here](./SOLUTIONS.md#conditional-rendering). +- Read the [React docs](https://reactjs.org/docs/hello-world.html) diff --git a/exercise-5/SOLUTIONS.md b/exercise-5/SOLUTIONS.md index 08edbd7..563c6b7 100644 --- a/exercise-5/SOLUTIONS.md +++ b/exercise-5/SOLUTIONS.md @@ -1,124 +1,64 @@ -# Possible solutions +# Possible Solutions -## Static HTML +## React Components ```jsx export default function Friends() { - return ( -
-

Hello, Friends!

-
- ); -} -``` - -## Evaluating a variable - -```jsx -export default function Friends() { - return ( -
-

Hello, Friends!

-

{greeting}

-
- ); -} -``` - -## Evaluating a function - -```jsx -export default function Friends() { - return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
- ); + return ; } ``` -## className +## Wrapping in a div ```jsx export default function Friends() { return (
-

Hello, Friends!

-

{emphasize(greeting)}

+

hello, friends!

+
); } ``` -## Evaluating attributes +## Wrapping in a fragment ```jsx export default function Friends() { return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
+ +

hello, friends!

+ +
); } ``` -## Array.map +## Arrays ```jsx export default function Friends() { - return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
    - {myFriends.map(friend => { - return
  • {friend.name}
  • ; - })} -
-
- ); + return myFriends.map(friend => ( + + )); } ``` -## Array keys +## Strings or numbers ```jsx -export default function Friends() { - return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
    - {myFriends.map(friend => { - return
  • {friend.name}
  • ; - })} -
-
- ); +function FriendProfile(props) { + return props.name; } ``` -## Conditional rendering +## null ```jsx -export default function Friends() { - return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
    - {myFriends.map(friend => { - return ( -
  • - {friend.name} - {friend.age ? ` (${friend.age})` : null} -
  • - ); - })} -
-
- ); +function FriendProfile(props) { + if (props.age === undefined) { + return null; + } + return props.name; } ``` diff --git a/exercise-5/complete/Exercise.js b/exercise-5/complete/Exercise.js index 9fb9ce4..2a69c7d 100644 --- a/exercise-5/complete/Exercise.js +++ b/exercise-5/complete/Exercise.js @@ -1,16 +1,16 @@ import React from 'react'; -const greeting = 'How are you?'; - -function emphasize(text) { - return `${text}!!!!!`; +export default function Friends() { + return myFriends.map(friend => ( + + )); } -function determineGreetingClass() { - if (new Date() % 2 === 1) { - return 'friends-greeting-orange'; +function FriendProfile(props) { + if (props.age === undefined) { + return null; } - return 'friends-greeting-purple'; + return props.name; } const myFriends = [ @@ -29,22 +29,3 @@ const myFriends = [ name: 'Turtle', }, ]; - -export default function Friends() { - return ( -
-

Hello, Friends!

-

{emphasize(greeting)}

-
    - {myFriends.map(friend => { - return ( -
  • - {friend.name} - {friend.age ? ` (${friend.age})` : null} -
  • - ); - })} -
-
- ); -} diff --git a/exercise-6/App.css b/exercise-6/App.css index 17df7f9..57d105a 100644 --- a/exercise-6/App.css +++ b/exercise-6/App.css @@ -14,14 +14,42 @@ line-height: 0.5em; } -.exercise { +.page { + display: flex; + flex-direction: row; + justify-content: center; + background: repeating-linear-gradient(to top, mintcream, teal); + height: calc(100vh - 140px); +} + +.content { + background-color: white; + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + height: calc(100vh - 140px); + width: 1000px; +} + +.card { + min-width: 150px; + background-color: mintcream; + margin: 20px; + padding: 30px; + box-shadow: 0 1px 2px teal; +} + +.card:hover { + box-shadow: 0 2px 4px teal; +} + +.friend-profile { display: flex; flex-direction: column; align-items: center; } -.friend-profile { - background-color: lavender; - margin: 10px; - padding: 5px; +.friend-profile h3 { + margin-bottom: 0; } diff --git a/exercise-6/App.js b/exercise-6/App.js index 9742b5c..85a4645 100644 --- a/exercise-6/App.js +++ b/exercise-6/App.js @@ -6,8 +6,8 @@ function App() { return (
-

Exercise 6

-

What can a component render?

+

Exercise 8

+

Composition & props.children

diff --git a/exercise-6/Exercise.js b/exercise-6/Exercise.js index dc6253d..2b91bda 100644 --- a/exercise-6/Exercise.js +++ b/exercise-6/Exercise.js @@ -1,14 +1,21 @@ import React from 'react'; -export default function Friends() { - return
; +export default function Exercise() { + return } -function FriendProfile(props) { +function Friends({friends}) { + return friends.map(friend => ( + + )); +} + + +function FriendProfile({name, image}) { return (
- {props.name} - {props.age ? ` (${props.age})` : null} + {name} +

{name}

); } @@ -17,15 +24,16 @@ const myFriends = [ { id: 1, name: 'Potatoes', - age: '4 months', + image: 'http://placekitten.com/150/150?image=1', }, { id: 2, name: 'Flower', - age: '6 months', + image: 'http://placekitten.com/150/150?image=12', }, { id: 3, name: 'Turtle', + image: 'http://placekitten.com/150/150?image=15', }, ]; diff --git a/exercise-6/README.md b/exercise-6/README.md index 7949182..aaed8c0 100644 --- a/exercise-6/README.md +++ b/exercise-6/README.md @@ -1,138 +1,131 @@ # Exercise 6 -## What can a component render? +## Composition & props.children -React function components are limited in what they can return. This exercise will introduce you to the 5 most frequently returned kinds of results. +Every React component receives a special prop named `children`. This prop contains any elements declared inside of the component. + +This exercise will give you experience working with the special `children` prop. 👉 Start the app for Exercise 6 In a console window, pointed at the root of this project, run `npm run start-exercise-6`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 6: What can a component render?". If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 6: Composition & props.children", and three adorable kittens. If it doesn't, ask your neighbor for assistance or raise your hand. 👉 Open Exercise.js All of your work for this exercise will take place in Exercise.js. -The return value of a React function component is what gets rendered to the DOM. Throughout this exercise, we'll use the terms "render" and "return" interchangeably. - -### Elements/React Components - -Most frequently, a component will render either an HTML element, or another React component. - -Currently, the `Friends` component is returning an HTML element - a single `
`. Most of the examples we've seen so far have returned HTML elements. - -There is a second component in `Exercise.js` - the `FriendProfile` component. - -👉 Modify the `Friends` component to return a single `` element. Pass the name of the first item in the `myFriends` array as a prop to the ``. - -Check your browser to see if you succeeded! You should see the name `Potatoes` rendered. - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#react-components). - -👉 "Inspect element" in your browser, and look for the elements rendered by the `` component. - -You should see that the component is rendered as a div with text content. - -### Fragments - -Valid JSX requires a single top-level element (similar to how valid XML requires a single top-level element). To demonstrate this, we're going to break our app. - -👉 Modify the `Friends` component to return an `

` element adjacent to the `` element. +### children Prop -You should see an error in your browser, similar to this: +Given the following component JSX... +```jsx +function Friends() { + return ( + +

My Friends!

+

Let's meet them.

+
+ ); +} ``` -./exercise-6/Exercise.js - Line 4: Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...? - - 2 | - 3 | export default function Friends() { -> 4 | return

hello, friends!

; - | ^ - 5 | } - 6 | - 7 | function FriendProfile(props) { -``` - -As the error indicates, this is invalid syntax. - -We can fix this a couple ways. - -#### Wrapping in a `
` - -👉 Modify the `Friends` component so that the `

` and `` elements are wrapped in a `
` element. - -Check your browser to see if you succeeded! You should see your title emitted, along with the name `Potatoes`. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#wrapping-in-a-div). +the `children` prop passed to the `` component will be an array of two elements - an `h1` and an `h2`. -👉 "Inspect element" in your browser, and find the elements rendered by the `` component. +When the `Page` component wants to render its children, it just needs to evaluate the `children` prop as an expression. For example: -You'll see that your components are wrapped in a `
` - the one you used to wrap the `

` and `` elements. +```jsx +function Page({ children }) { + return
{children}
; +} +``` -This solves the problem...but it adds elements to the DOM that we don't really need. One element might not seem like a big deal, but in a large React app, we can end up with significant DOM pollution from this practice. +would wrap the children in a `div` with class `page`. -React v16 introduced a way to solve this problem - the `Fragment`. +When the `Friends` component above is rendered to the DOM, it will render the following markup: -#### Wrapping in a `` +```html +
+

My Friends!

+

Let's meet them.

+
+``` -The `Fragment` component is basically an empty wrapper. It allows us to provide a single top-level element when we are rendering multiple child elements, but it does not actually render anything to the DOM. +Now it's your turn. Let's build some components that wrap their children. -You can access the `Fragment` component on the default `React` import (i.e. `React.Fragment`). +The App.css for this exercise contains all the styles you'll need, if you define the components correctly. -👉 Replace the wrapping `
` element in the `Friends` component with a wrapping `` element. +### `` Component -Check your browser to see if you succeeded! You should still see all friends listed. +Let's write a Page component that we will use to wrap the content on our page. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#wrapping-in-a-fragment). +👉 Add a component named `Page` to `Exercise.js` that wraps its children in `.page` and `.content` `div`s. -👉 "Inspect element" in your browser, and find the elements rendered by the `` component. +For example, if a component rendered -You should see that there's no longer an unnecessary div wrapping your output. +```html + +

Title

+
+``` -### Arrays +then the markup emitted should be -Sometimes you want to turn an array of data into an array of elements. This can be accomplished with `Array.map`, as we saw in the JSX Fundamentals exercise. +```html +
+
+

Title

+
+
+``` -👉 Modify the `Friends` component to return one `` element for each item in the `myFriends` array. For each item, pass the `name` and `age` as props to the ``. +You won't notice any changes in your browser, as we're not using the `` component yet. -Don't forget that React wants you to use a `key` prop when you render an array of items! +If you get stuck, [see a possible solution here](./SOLUTIONS.md#page). -Check your browser to see if you succeeded! You should see each of the friends listed. +👉 Modify the `Friends` component to wrap the mapped `` elements in a `` element. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#arrays). +You should notice a difference in the browser this time. If you got everything right, you'll have green gradients down the sides of the page. In addition, our kitten friends will be lined up horizontally instead of vertically. -👉 "Inspect element" in your browser, and find the elements rendered by the `` component. +![](docs/pages.png) -You should see a `
` for each friend. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-with-page). -### Strings/Numbers +### `` Component -Sometimes you want a component to render nothing more than a string or number. When a React function component returns a string or number, it gets rendered as a text node in the DOM. +Our kittens look a little close together. Let's wrap them in `` components, to give them a bit more distinction. -👉 Modify the `FriendProfile` component to return only the `name` prop. +👉 Add a component named `Card` to `Exercise.js` that wraps its children in a `.card` `div`. -Check your browser to see if you succeeded! You should see each of the friends listed, abutted against each other. +For example, if a component rendered -If you get stuck, [see a possible solution here](./SOLUTIONS.md#strings-or-numbers). +```html + +

Title

+
+``` -👉 "Inspect element" in your browser, and find the elements rendered by the `` component. +then the markup emitted should be -You will see that the friend names are rendered as text nodes, all within the same `
`. +```html +
+

Title

+
+``` -### null +You won't notice any changes in your browser, as we're not using the `` component yet. -Sometimes we don't want a component to render anything at all. This is usually true when we are using conditional logic to render different results based on inputs. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#card). -When the value `null` is returned from a component, nothing gets rendered to the DOM. +👉 Modify the `FriendProfile` component to wrap the `.friend-profile` div in a `` element. -👉 Modify the `FriendProfile` component to return `null` if the `age` prop is undefined; otherwise return the value of the `name` prop. +You should notice a difference in the browser this time. If you got everything right, each of our kitten friends will have a nice card element separating them from the rest. -Check your browser to see if you succeeded! You should see two of the friends listed - Potatoes and Flower. +![Finished](docs/pages-and-cards.png) -If you get stuck, [see a possible solution here](./SOLUTIONS.md#null). +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-with-card). ### Extra Credit -- Read the [React docs](https://reactjs.org/docs/hello-world.html) +Read more about [composition vs. inheritance in React](https://reactjs.org/docs/composition-vs-inheritance.html) diff --git a/exercise-6/SOLUTIONS.md b/exercise-6/SOLUTIONS.md index 563c6b7..c387e54 100644 --- a/exercise-6/SOLUTIONS.md +++ b/exercise-6/SOLUTIONS.md @@ -1,64 +1,54 @@ # Possible Solutions -## React Components +## Page ```jsx -export default function Friends() { - return ; -} -``` - -## Wrapping in a div - -```jsx -export default function Friends() { +function Page({ children }) { return ( -
-

hello, friends!

- +
+
{children}
); } ``` -## Wrapping in a fragment +## Friends With Page ```jsx -export default function Friends() { +export default function Friends({friends}) { return ( - -

hello, friends!

- -
+ + {friends.map(friend => ( + + ))} + ); } ``` -## Arrays - -```jsx -export default function Friends() { - return myFriends.map(friend => ( - - )); -} -``` - -## Strings or numbers +## Card ```jsx -function FriendProfile(props) { - return props.name; +function Card({ children }) { + return
{children}
; } ``` -## null +## FriendProfile With Card ```jsx -function FriendProfile(props) { - if (props.age === undefined) { - return null; - } - return props.name; +function FriendProfile({ name, image }) { + return ( + +
+ {name} +

{name}

+
+
+ ); } ``` diff --git a/exercise-6/complete/Exercise.js b/exercise-6/complete/Exercise.js index 2a69c7d..8e3a165 100644 --- a/exercise-6/complete/Exercise.js +++ b/exercise-6/complete/Exercise.js @@ -1,31 +1,60 @@ import React from 'react'; -export default function Friends() { - return myFriends.map(friend => ( - - )); +export default function Exercise() { + return ; } -function FriendProfile(props) { - if (props.age === undefined) { - return null; - } - return props.name; +function Friends({ friends }) { + return ( + + {friends.map(friend => ( + + ))} + + ); +} + +function Page({ children }) { + return ( +
+
{children}
+
+ ); +} + +function Card({ children }) { + return
{children}
; +} + +function FriendProfile({ name, image }) { + return ( + +
+ {name} +

{name}

+
+
+ ); } const myFriends = [ { id: 1, name: 'Potatoes', - age: '4 months', + image: 'http://placekitten.com/150/150?image=1', }, { id: 2, name: 'Flower', - age: '6 months', + image: 'http://placekitten.com/150/150?image=12', }, { id: 3, name: 'Turtle', + image: 'http://placekitten.com/150/150?image=15', }, ]; diff --git a/exercise-8/docs/pages-and-cards.png b/exercise-6/docs/pages-and-cards.png similarity index 100% rename from exercise-8/docs/pages-and-cards.png rename to exercise-6/docs/pages-and-cards.png diff --git a/exercise-8/docs/pages.png b/exercise-6/docs/pages.png similarity index 100% rename from exercise-8/docs/pages.png rename to exercise-6/docs/pages.png diff --git a/exercise-10/1-component-css-files/App.css b/exercise-7/1-component-css-files/App.css similarity index 100% rename from exercise-10/1-component-css-files/App.css rename to exercise-7/1-component-css-files/App.css diff --git a/exercise-10/1-component-css-files/App.js b/exercise-7/1-component-css-files/App.js similarity index 100% rename from exercise-10/1-component-css-files/App.js rename to exercise-7/1-component-css-files/App.js diff --git a/exercise-10/1-component-css-files/Card.css b/exercise-7/1-component-css-files/Card.css similarity index 100% rename from exercise-10/1-component-css-files/Card.css rename to exercise-7/1-component-css-files/Card.css diff --git a/exercise-10/1-component-css-files/Card.js b/exercise-7/1-component-css-files/Card.js similarity index 100% rename from exercise-10/1-component-css-files/Card.js rename to exercise-7/1-component-css-files/Card.js diff --git a/exercise-10/1-component-css-files/Exercise.js b/exercise-7/1-component-css-files/Exercise.js similarity index 100% rename from exercise-10/1-component-css-files/Exercise.js rename to exercise-7/1-component-css-files/Exercise.js diff --git a/exercise-10/1-component-css-files/FriendProfile.css b/exercise-7/1-component-css-files/FriendProfile.css similarity index 100% rename from exercise-10/1-component-css-files/FriendProfile.css rename to exercise-7/1-component-css-files/FriendProfile.css diff --git a/exercise-10/1-component-css-files/FriendProfile.js b/exercise-7/1-component-css-files/FriendProfile.js similarity index 100% rename from exercise-10/1-component-css-files/FriendProfile.js rename to exercise-7/1-component-css-files/FriendProfile.js diff --git a/exercise-10/1-component-css-files/Friends.js b/exercise-7/1-component-css-files/Friends.js similarity index 100% rename from exercise-10/1-component-css-files/Friends.js rename to exercise-7/1-component-css-files/Friends.js diff --git a/exercise-10/1-component-css-files/Page.css b/exercise-7/1-component-css-files/Page.css similarity index 100% rename from exercise-10/1-component-css-files/Page.css rename to exercise-7/1-component-css-files/Page.css diff --git a/exercise-10/1-component-css-files/Page.js b/exercise-7/1-component-css-files/Page.js similarity index 100% rename from exercise-10/1-component-css-files/Page.js rename to exercise-7/1-component-css-files/Page.js diff --git a/exercise-10/2-css-modules/App.js b/exercise-7/2-css-modules/App.js similarity index 100% rename from exercise-10/2-css-modules/App.js rename to exercise-7/2-css-modules/App.js diff --git a/exercise-10/2-css-modules/App.module.css b/exercise-7/2-css-modules/App.module.css similarity index 100% rename from exercise-10/2-css-modules/App.module.css rename to exercise-7/2-css-modules/App.module.css diff --git a/exercise-10/2-css-modules/Card.js b/exercise-7/2-css-modules/Card.js similarity index 100% rename from exercise-10/2-css-modules/Card.js rename to exercise-7/2-css-modules/Card.js diff --git a/exercise-10/2-css-modules/Card.module.css b/exercise-7/2-css-modules/Card.module.css similarity index 100% rename from exercise-10/2-css-modules/Card.module.css rename to exercise-7/2-css-modules/Card.module.css diff --git a/exercise-10/2-css-modules/Exercise.js b/exercise-7/2-css-modules/Exercise.js similarity index 100% rename from exercise-10/2-css-modules/Exercise.js rename to exercise-7/2-css-modules/Exercise.js diff --git a/exercise-10/2-css-modules/FriendProfile.js b/exercise-7/2-css-modules/FriendProfile.js similarity index 100% rename from exercise-10/2-css-modules/FriendProfile.js rename to exercise-7/2-css-modules/FriendProfile.js diff --git a/exercise-14/complete/friends/FriendProfile.module.css b/exercise-7/2-css-modules/FriendProfile.module.css similarity index 100% rename from exercise-14/complete/friends/FriendProfile.module.css rename to exercise-7/2-css-modules/FriendProfile.module.css diff --git a/exercise-10/2-css-modules/Friends.js b/exercise-7/2-css-modules/Friends.js similarity index 100% rename from exercise-10/2-css-modules/Friends.js rename to exercise-7/2-css-modules/Friends.js diff --git a/exercise-10/2-css-modules/Page.js b/exercise-7/2-css-modules/Page.js similarity index 100% rename from exercise-10/2-css-modules/Page.js rename to exercise-7/2-css-modules/Page.js diff --git a/exercise-10/2-css-modules/Page.module.css b/exercise-7/2-css-modules/Page.module.css similarity index 100% rename from exercise-10/2-css-modules/Page.module.css rename to exercise-7/2-css-modules/Page.module.css diff --git a/exercise-10/3-styled-components/App.js b/exercise-7/3-styled-components/App.js similarity index 100% rename from exercise-10/3-styled-components/App.js rename to exercise-7/3-styled-components/App.js diff --git a/exercise-10/3-styled-components/Card.js b/exercise-7/3-styled-components/Card.js similarity index 100% rename from exercise-10/3-styled-components/Card.js rename to exercise-7/3-styled-components/Card.js diff --git a/exercise-10/3-styled-components/Exercise.js b/exercise-7/3-styled-components/Exercise.js similarity index 100% rename from exercise-10/3-styled-components/Exercise.js rename to exercise-7/3-styled-components/Exercise.js diff --git a/exercise-10/3-styled-components/FriendProfile.js b/exercise-7/3-styled-components/FriendProfile.js similarity index 100% rename from exercise-10/3-styled-components/FriendProfile.js rename to exercise-7/3-styled-components/FriendProfile.js diff --git a/exercise-10/3-styled-components/Friends.js b/exercise-7/3-styled-components/Friends.js similarity index 100% rename from exercise-10/3-styled-components/Friends.js rename to exercise-7/3-styled-components/Friends.js diff --git a/exercise-10/3-styled-components/Page.js b/exercise-7/3-styled-components/Page.js similarity index 100% rename from exercise-10/3-styled-components/Page.js rename to exercise-7/3-styled-components/Page.js diff --git a/exercise-10/3-styled-components/theme.js b/exercise-7/3-styled-components/theme.js similarity index 100% rename from exercise-10/3-styled-components/theme.js rename to exercise-7/3-styled-components/theme.js diff --git a/exercise-10/Page.js b/exercise-7/Page.js similarity index 100% rename from exercise-10/Page.js rename to exercise-7/Page.js diff --git a/exercise-7/README.md b/exercise-7/README.md index 0f626bb..cead645 100644 --- a/exercise-7/README.md +++ b/exercise-7/README.md @@ -1,104 +1,111 @@ # Exercise 7 -## Convert A Component -Components that don't maintain their own state or use React methods other than `render()` can be converted from class syntax components to stateless functional components. This reduces code boilerplate. +## Three Ways To Style -In this exercise, you'll convert a component from class syntax to stateless functional. This is an activity that happens often in React development. +There are many ways to style a React app. In this exercise, you'll compare and contrast three different ways. 👉 Start the app for Exercise 7 In a console window, pointed at the root of this project, run `npm run start-exercise-7`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 7: Convert a Component". If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 7: Styling - Component CSS Files", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. -👉 Open Exercise.js +### Background -All of your work for this exercise will take place in Exercise.js. +One thing is common among most ways to style a React app: they involve co-locating your CSS with your components. -### Background +In this exercise, we've styled the app three different ways - with component CSS files, CSS Modules, and Styled Components. + +### Component CSS Files + +The component CSS file method of styling an app involves putting styles for a component in an associated CSS file. + +#### Explore Component CSS Files + +👉 Open the `/1-component-css-files` folder. + +In it, you'll see that each component `___.js` has an associated `___.css` file. Each of these css files contains the styles for its component, and its component only. + +👉 Open a component `.js` file and its corresponding `.css` file. + +👉 Answer some questions. + +1. How does a component CSS file get included in our app? [See here for answer](SOLUTIONS.md#1-1) + +2. How does this method of including a dependency differ from the other dependencies included in a component file? [See here for answer](SOLUTIONS.md#1-2) + +### CSS Modules + +CSS Modules attempt to solve a frustrating problem that many developers struggle with - CSS scope conflicts. + +CSS Modules look like a similar strategy to Component CSS Files, in that there is a single CSS file associated with each component. However, a CSS Module gets compiled into a set of styles that are locally scoped, using a unique hash for every component. When a CSS Module is compiled, it becomes a JavaScript object, with properties for every style defined in the CSS Module. A JavaScript file that imports a CSS Module can reference the styles in the module via properties on the import. + +If you'd like to read more about CSS Modules, visit https://github.com/css-modules/css-modules. + +#### Switch to a CSS Module implementation + +👉 In `index.js`, comment out line 4 (which tells the app to use component CSS files), and uncomment line 5 (which tells the app to use CSS Modules). -In this exercise, we have two components in `Exercise.js` - `Friends` and `FriendProfile`. Neither of these components needs state, and neither needs any React methods other than `render()`. We can convert them both! +You should see the color of the app change from green to purple in your browser, and the subtitle should change to indicate it is using CSS Modules. -### The Process +#### Explore CSS Modules -When converting to a stateless functional component, there are three things that need to be done: +👉 Open the `/2-css-modules` folder. -#### 1. Convert the class to a function of the same name +Inside this folder you'll see that again, there is a single `.css` file for each component. -For example, +Notice that every CSS file name ends with `.module.css`. This is an implementation detail of create-react-app, the tool that was used to generate the app. This naming convention is not required to use CSS Modules in general, but is required to use them with create-react-app. -```jsx -class FriendProfile extends React.Component { - // render()... -} -``` +👉 Open a component `.js` file and its corresponding `.module.css` file. -becomes +👉 Answer some questions. -```jsx -function FriendProfile(props) { - // render()... -} -``` +1. How does a CSS Module get included in our app? How is this different from a component CSS file? [See here for answer](SOLUTIONS.md#2-1) -#### 2. Remove the render() method +2. How is a style in a CSS Module used in a component? [See here for answer](SOLUTIONS.md#2-2) -For example, +3. On line 13 of `App.js`, there is a call to a function named `classNames`. What do you think this statement is doing? [See here for answer](SOLUTIONS.md#2-3) -```jsx -function FriendProfile(props) { - render() { - // ...what gets rendered - } -} -``` +4. In your browser, inspect an element that is styled with a CSS Module. What do you notice about the class associated with the element, and why do you think that is? [See here for answer](SOLUTIONS.md#2-4) -becomes +### Styled Components -```jsx -function FriendProfile(props) { - // ...what gets rendered -} -``` +Styled Components are very different from the first two approaches. Instead of having a separate CSS file for every component, all styling happens inside of each component - as JavaScript. -#### 3. Convert `this.props` references to `props`. +To define styles for a component with Styled Components, you create a React component that defines nothing but styles, then render it as a child of your component. -For example, +Each Styled Component is compiled, and uses local scope like CSS Modules - so it also solves the frustrating problem of CSS scope conflicts. -```jsx - {this.props.name} -``` +#### Switch to a Styled Components implementation -becomes +👉 In `index.js`, comment out line 5 (which tells the app to use CSS Modules), and uncomment line 6 (which tells the app to use Styled Components). -```jsx - {props.name} -``` +You should see the color of the app change from purple to green in your browser, and the subtitle should change to indicate it is using Styled Components. -That's all it takes to convert from a class syntax component to a stateless functional one! +#### Explore Styled Components -👉 Convert the `Friends` component to a stateless functional component +👉 Open the `/3-styled-components` folder. -Check your browser to make sure the components are still rendering properly! +Inside this folder you'll see that there are no `.css` files. This is because all component styles are within each `.js` file. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-to-stateless). +👉 Open the `App.js` file. -👉 Convert the `FriendProfile` component to a stateless functional component +On line 2, we `import styled from 'styled-components'`. This is the library we use to build Styled Components. -Check your browser to make sure the components are still rendering properly! +The `styled` object exposes one helper for any HTML element that you would want to style. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-to-stateless). +On lines 7-9, we use the `styled.div` helper to create a `div` element with style `text-align: center`. But immediately following the word `div` is a pair of backticks (`), with what looks like CSS inside them. -Sometimes, we need to convert the other direction - from stateless functional component to class syntax component. +This is called a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates). Tagged templates are a newer feature of JavaScript that allow you to parse a template literal with a function. In this example, the function is the `div` helper of `styled`. The template literal being parsed is the content between the backticks immediately following the word `div`. -👉 Convert the `Friends` component back to a class syntax component +The contents of the template literal for a Styled Component are the CSS that you want to apply to the element. -Check your browser to make sure the components are still rendering properly! +We looked at template literals earlier in the workshop. Remember that you can inject values in a template literal with `${...}`. This means we can inject any JavaScript into the CSS for an element! -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-to-class). +👉 Answer some questions. -👉 Convert the `FriendProfile` component back to a class syntax component +1. How is the `AppDiv` component used? How do you think this element will render in the browser? [See here for answer](SOLUTIONS.md#3-1) -Check your browser to make sure the components are still rendering properly! +2. In your browser, use the dev tools to find the `AppDiv` element. What does the class name look like? [See here for answer](SOLUTIONS.md#3-2) -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-to-class). +3. On line 14 of `App.js`, in the `AppHeader` component, we are using string interpolation to inject the value `theme.white`. What do you think is the intention of this code? [See here for answer](SOLUTIONS.md#3-3) diff --git a/exercise-7/SOLUTIONS.md b/exercise-7/SOLUTIONS.md index fcebc55..22860a6 100644 --- a/exercise-7/SOLUTIONS.md +++ b/exercise-7/SOLUTIONS.md @@ -1,51 +1,63 @@ -# Possible Solutions +# Answers -## Friends to stateless +## 1. Component CSS Files -```jsx -export default function Friends(props) { - return myFriends.map(friend => ( - - )); -} -``` +### 1-1 -## FriendProfile to stateless - -```jsx -function FriendProfile(props) { - return ( -
- {props.name} - {props.age ? ` (${props.age})` : null} -
- ); -} -``` +CSS files are included in each component using `import '___.css'`. + +When the app is compiled with webpack, this import indicates that the component has a dependency on the CSS file, and it will include it in the build. + +### 1-2 + +Imported JavaScript dependencies include an identifier for the module being imported (i.e. `import dependency from 'dependency'), while imported CSS files do not (i.e.`import 'styles.css'`). + +This is because JavaScript dependencies are only imported if your code needs to interact with them. CSS dependencies aren't interacted with, so they don't need an identifier. + +## 2. CSS Modules + +### 2-1 + +A CSS Module is imported into a component using `import styles from '___.module.css'`. This is different from component CSS files in that we are giving the imported module an identifier - `styles`. + +### 2-2 + +This `styles` object imported from the CSS Module contains properties for every class we define in the `styles.css` file. We use that object to specify which style from the module to use on an element. + +For example, if the CSS Module contained a style `.title { ... }`, in the associated component we would use it with `

`. + +### 2-3 -## Friends to class +The `classNames` library is used to combine CSS Module class names. -```jsx -export default class Friends extends React.Component { - render() { - return myFriends.map(friend => ( - - )); - } -} +In the example specified, we are saying that this element should use both the `subTitle` and `emphasize` styles. + +We could probably combine these names ourselves, without the `classNames` library. `classNames` is a handy helper, though, and can also be used to combine styles conditionally. If you're using CSS Modules in a project, there's a good chance you'll also want to use `classNames`. + +### 2-4 + +The class name looks generated, and contains a hash, like this - `Card_card__9mB3O`. + +This is because CSS Modules is compiling our styles, and it gives each class a unique name to avoid conflicts. + +## Styled Components + +### 3-1 + +On line 24, we render the `AppDiv` element in our `App` component, like this: + +``` + + ... + ``` -## FriendProfile to class - -```jsx -class FriendProfile extends React.Component { - render() { - return ( -
- {this.props.name} - {this.props.age ? ` (${this.props.age})` : null} -
- ); - } -} -``` \ No newline at end of file +We can do this, because a Styled Component is just another React component! + +### 3-2 + +Similarly to the CSS Module class, the Styled Component class name contains a hash. As you might expect, this is so that the class is uniquely named, and we don't have to deal with style conflicts. + +### 3-3 + +The theme colors are defined in a separate JavaScript file, and we are injecting them here! This would be a great way to manage themes in one place, and have them consumed throughout the app. If we decide the theme colors need to change, we can change them in one place, instead of a bunch of CSS files! diff --git a/exercise-7/index.js b/exercise-7/index.js index 395b749..396e44c 100644 --- a/exercise-7/index.js +++ b/exercise-7/index.js @@ -1,6 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import App from './App'; +import App from './1-component-css-files/App'; +// import App from './2-css-modules/App'; +// import App from './3-styled-components/App'; ReactDOM.render(, document.getElementById('root')); diff --git a/exercise-8/App.css b/exercise-8/App.css deleted file mode 100644 index 57d105a..0000000 --- a/exercise-8/App.css +++ /dev/null @@ -1,55 +0,0 @@ -.App { - text-align: center; -} - -.App-header { - background-color: teal; - height: 100px; - padding: 20px; - color: white; -} - -.App-title { - font-family: 'Pacifico', cursive; - line-height: 0.5em; -} - -.page { - display: flex; - flex-direction: row; - justify-content: center; - background: repeating-linear-gradient(to top, mintcream, teal); - height: calc(100vh - 140px); -} - -.content { - background-color: white; - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - height: calc(100vh - 140px); - width: 1000px; -} - -.card { - min-width: 150px; - background-color: mintcream; - margin: 20px; - padding: 30px; - box-shadow: 0 1px 2px teal; -} - -.card:hover { - box-shadow: 0 2px 4px teal; -} - -.friend-profile { - display: flex; - flex-direction: column; - align-items: center; -} - -.friend-profile h3 { - margin-bottom: 0; -} diff --git a/exercise-8/App.js b/exercise-8/App.js index 85a4645..cd0b14f 100644 --- a/exercise-8/App.js +++ b/exercise-8/App.js @@ -1,18 +1,23 @@ import React from 'react'; -import './App.css'; -import Exercise from './Exercise'; + +import { BrowserRouter, Route } from 'react-router-dom'; + +import Friends from './friends/Friends.entry'; +import styles from './App.module.css'; function App() { return ( -
-
-

Exercise 8

-

Composition & props.children

-
-
- + +
+
+

Exercise 11

+

React Router

+
+
+ +
-
+ ); } diff --git a/exercise-14/App.module.css b/exercise-8/App.module.css similarity index 100% rename from exercise-14/App.module.css rename to exercise-8/App.module.css diff --git a/exercise-8/Exercise.js b/exercise-8/Exercise.js deleted file mode 100644 index 2b91bda..0000000 --- a/exercise-8/Exercise.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -export default function Exercise() { - return -} - -function Friends({friends}) { - return friends.map(friend => ( - - )); -} - - -function FriendProfile({name, image}) { - return ( -
- {name} -

{name}

-
- ); -} - -const myFriends = [ - { - id: 1, - name: 'Potatoes', - image: 'http://placekitten.com/150/150?image=1', - }, - { - id: 2, - name: 'Flower', - image: 'http://placekitten.com/150/150?image=12', - }, - { - id: 3, - name: 'Turtle', - image: 'http://placekitten.com/150/150?image=15', - }, -]; diff --git a/exercise-8/README.md b/exercise-8/README.md index 55b1d76..8f9fe80 100644 --- a/exercise-8/README.md +++ b/exercise-8/README.md @@ -1,134 +1,86 @@ # Exercise 8 -## Composition & props.children -Every React component receives a special prop named `children`. This prop contains any elements declared inside of the component. +## React Router -This exercise will give you experience working with the special `children` prop. +In this exercise, we're going to use React-Router to build a second page into our app. When we're complete, users will be able to navigate from our friends list to a detailed view of each friend. 👉 Start the app for Exercise 8 In a console window, pointed at the root of this project, run `npm run start-exercise-8`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 8: Composition & props.children", and three adorable kittens. If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 8: React Router", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. -👉 Open Exercise.js +### Component Reorganization -All of your work for this exercise will take place in Exercise.js. +In this exercise, we've re-architected our file & folder structure. We've created some folders: -### children Prop +- `/data`: this folder holds the data that drives our app. For now, that's our list of friends. +- `/friend-detail`: this folder holds components that render a "friend detail" page. This is a new page we'll be building out in this exercise. +- `/friends`: this folder holds components that render the "friends list" page - the one we've been working with so far. +- `/shared`: this folder contains components that could be shared across multiple other folders. -Given the following component JSX... +### Router Setup -```jsx -function Friends() { - return ( - -

My Friends!

-

Let's meet them.

-
- ); -} -``` +To set up React-Router, we've already taken a few steps: -the `children` prop passed to the `` component will be an array of two elements - an `h1` and an `h2`. +- Installed React-Router as a dependency, by running `npm install --save react-router` +- Wrapped our app in a `` component, in `App.js` +- Defined a route in `App.js` that matches the path `/` exactly, and renders our `` entry component. -When the `Page` component wants to render its children, it just needs to evaluate the `children` prop as an expression. For example: +### Add a new `` -```jsx -function Page({children}) { - return ( -
- {children} -
- ); -} -``` +In the folder `/friend-detail`, we've added a couple components that render a very simple Friend Detail page. -would wrap the children in a `div` with class `page`. +👉 Add a new `` to `App.js`, which will render the new Friend Detail page. -When the `Friends` component above is rendered to the DOM, it will render the following markup: +You'll want to import the `` component from './friend-detail/FriendDetail.entry'. -```html -
-

My Friends!

-

Let's meet them.

-
-``` +The route should match the path `friends/:id`. The token `:id` indicates that this route will have an `id` parameter submitted in the path. -Now it's your turn. Let's build some components that wrap their children. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#frienddetail-route). -The App.css for this exercise contains all the styles you'll need, if you define the components correctly. +### Add a link to the new Route -### `` Component +We need a way to navigate to the route we added. -Let's write a Page component that we will use to wrap the content on our page. +React-Router includes a `` component for navigating to a router-friendly URL. -👉 Add a component named `Page` to `Exercise.js` that wraps its children in `.page` and `.content` `div`s. +👉 Wrap the `` element in `'/friends/FriendProfile.js` in a `` element. -For example, if a component rendered +Remember that you'll need to import the `Link` component from `react-router-dom`, and that the `to` prop is where the link will go when it is clicked. -```html - -

Title

-
-``` +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-link). -then the markup emitted should be +### Get friend ID from the URL -```html -
-
-

Title

-
-
-``` +When you click on one of our kitten friends, you should navigate to the Friend Detail page! -You won't notice any changes in your browser, as we're not using the `` component yet. +Unfortunately, regardless of which kitten you click, it always renders the same friend - Turtle. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#page). +👉 Open `/friend-detail/FriendDetail.entry.js`, and see if you can identify why we are always rendering Turtle. -👉 Modify the `Friends` component to wrap the mapped `` elements in a `` element. +...(come back when you've figured it out, or you give up)... -You should notice a difference in the browser this time. If you got everything right, you'll have green gradients down the sides of the page. In addition, our kitten friends will be lined up horizontally instead of vertically. +As the comment in the component indicates, we aren't getting the active friend ID in this component. -![](docs/pages.png) +When we use React-Router, our components automatically get access to a prop named `match`. This `match` prop contains a `params` array, which contains all the parameters passed into the current route. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-with-page). +👉 Modify `/friend-detail/FriendDetail.entry.js` so that it pulls the active friend ID from the `match` prop. -### `` Component +Use `console.log` to inspect the props passed into the component if you need to. -Our kittens look a little close together. Let's wrap them in `` components, to give them a bit more distinction. +You'll want to use `friends.find(...)` to find the friend whose id property matches the ID passed in. -👉 Add a component named `Card` to `Exercise.js` that wraps its children in a `.card` `div`. +One other trick - the `id` parameter in the `match.params` array is a string; the `id` properties in the `friends` array are integers. You could use `parseInt(...)` to convert between the two types. -For example, if a component rendered +When you've completed this task, you should be able to navigate to all three kitten friends' detail pages. -```html - -

Title

-
-``` +If you get stuck, [see a possible solution here](./SOLUTIONS.md#active-friend-id). -then the markup emitted should be +### Return to Home -```html -
-

Title

-
-``` +We can use the browser navigation to go back to the Home page, but it'd be really helpful if we could add a link to the "Home" page on the Friend Detail page. -You won't notice any changes in your browser, as we're not using the `` component yet. +👉 Add a Link to `/friend-detail/FriendDetail.js` that takes us back to the Home page. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#card). - -👉 Modify the `FriendProfile` component to wrap the `.friend-profile` div in a `` element. - -You should notice a difference in the browser this time. If you got everything right, each of our kitten friends will have a nice card element separating them from the rest. - -![Finished](docs/pages-and-cards.png) - -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-with-card). - -### Extra Credit - -Read more about [composition vs. inheritance in React](https://reactjs.org/docs/composition-vs-inheritance.html) \ No newline at end of file +If you get stuck, [see a possible solution here](./SOLUTIONS.md#home-link). diff --git a/exercise-8/SOLUTIONS.md b/exercise-8/SOLUTIONS.md index c387e54..53d70bc 100644 --- a/exercise-8/SOLUTIONS.md +++ b/exercise-8/SOLUTIONS.md @@ -1,54 +1,82 @@ # Possible Solutions -## Page +## FriendDetail Route ```jsx -function Page({ children }) { +import { BrowserRouter, Route } from 'react-router-dom'; +import FriendDetail from './friend-detail/FriendDetail.entry'; +... + +function App() { return ( -
-
{children}
-
+ +
+
+

Exercise 11

+

React Router

+
+
+ + +
+
+
); } + ``` -## Friends With Page +## FriendProfile Link ```jsx -export default function Friends({friends}) { +import { Link } from 'react-router-dom'; +... + +export default function FriendProfile({ id, name, image }) { return ( - - {friends.map(friend => ( - - ))} - + + +
+ {name} +

{name}

+
+
+ ); } ``` -## Card +## Active friend ID ```jsx -function Card({ children }) { - return
{children}
; +export default function({ match }) { + // the match prop is passed in via react.router + const friendId = match.params.id; + const friend = friends.find(x => x.id === parseInt(friendId, 10)); + + return ; } ``` -## FriendProfile With Card +## Home Link ```jsx -function FriendProfile({ name, image }) { +export default function({ friend }) { return ( - -
- {name} -

{name}

+ +
+
+ < Home +
+ +
+

{friend.name}

+ +

{friend.bio}

+
+
- +
); } ``` diff --git a/exercise-14/complete/App.js b/exercise-8/complete/App.js similarity index 83% rename from exercise-14/complete/App.js rename to exercise-8/complete/App.js index 5ef2e8b..d9bcaab 100644 --- a/exercise-14/complete/App.js +++ b/exercise-8/complete/App.js @@ -12,8 +12,8 @@ function App() {
-

Exercise 14

-

Loading Data

+

Exercise 11

+

React Router

diff --git a/exercise-14/complete/App.module.css b/exercise-8/complete/App.module.css similarity index 100% rename from exercise-14/complete/App.module.css rename to exercise-8/complete/App.module.css diff --git a/exercise-8/complete/Exercise.js b/exercise-8/complete/Exercise.js deleted file mode 100644 index 8e3a165..0000000 --- a/exercise-8/complete/Exercise.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; - -export default function Exercise() { - return ; -} - -function Friends({ friends }) { - return ( - - {friends.map(friend => ( - - ))} - - ); -} - -function Page({ children }) { - return ( -
-
{children}
-
- ); -} - -function Card({ children }) { - return
{children}
; -} - -function FriendProfile({ name, image }) { - return ( - -
- {name} -

{name}

-
-
- ); -} - -const myFriends = [ - { - id: 1, - name: 'Potatoes', - image: 'http://placekitten.com/150/150?image=1', - }, - { - id: 2, - name: 'Flower', - image: 'http://placekitten.com/150/150?image=12', - }, - { - id: 3, - name: 'Turtle', - image: 'http://placekitten.com/150/150?image=15', - }, -]; diff --git a/exercise-14/data/friends.js b/exercise-8/complete/data/friends.js similarity index 67% rename from exercise-14/data/friends.js rename to exercise-8/complete/data/friends.js index 7ba5db0..5fb4b02 100644 --- a/exercise-14/data/friends.js +++ b/exercise-8/complete/data/friends.js @@ -3,21 +3,18 @@ export default [ id: 1, name: 'Potatoes', image: 'http://placekitten.com/150/150?image=1', - bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.', - colors: [ 'Gray', 'Black' ] + bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.' }, { id: 2, name: 'Flower', image: 'http://placekitten.com/150/150?image=12', - bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.', - colors: [ 'Gray', 'Black', 'White' ] + bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.' }, { id: 3, name: 'Turtle', image: 'http://placekitten.com/150/150?image=15', - bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.', - colors: [ 'Gray', 'White' ] + bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.' }, ]; diff --git a/exercise-14/friend-detail/FriendDetail.entry.js b/exercise-8/complete/friend-detail/FriendDetail.entry.js similarity index 100% rename from exercise-14/friend-detail/FriendDetail.entry.js rename to exercise-8/complete/friend-detail/FriendDetail.entry.js diff --git a/exercise-15/friend-detail/FriendDetail.js b/exercise-8/complete/friend-detail/FriendDetail.js similarity index 54% rename from exercise-15/friend-detail/FriendDetail.js rename to exercise-8/complete/friend-detail/FriendDetail.js index 33c3aec..fad1d4e 100644 --- a/exercise-15/friend-detail/FriendDetail.js +++ b/exercise-8/complete/friend-detail/FriendDetail.js @@ -2,23 +2,9 @@ import React from 'react'; import { Link } from 'react-router-dom'; import Page from '../shared/Page'; import Card from '../shared/Card'; -import FriendFlipper from './FriendFlipper'; import styles from './FriendDetail.module.css'; -function renderFriend(friend) { - if (friend === undefined) { - return

Loading...

; - } - - return ( -
-

{friend.name}

- -

{friend.bio}

-
- ); -} export default function({ friend }) { return ( @@ -26,7 +12,12 @@ export default function({ friend }) {
< Home
- {renderFriend(friend)} + +

{friend.name}

+

ID: {friend.id}

+ +

{friend.bio}

+
); diff --git a/exercise-8/complete/friend-detail/FriendDetail.module.css b/exercise-8/complete/friend-detail/FriendDetail.module.css new file mode 100644 index 0000000..3432a3e --- /dev/null +++ b/exercise-8/complete/friend-detail/FriendDetail.module.css @@ -0,0 +1,3 @@ +.friendDetail { + width: 50%; +} diff --git a/exercise-14/friends/FriendProfile.js b/exercise-8/complete/friends/FriendProfile.js similarity index 100% rename from exercise-14/friends/FriendProfile.js rename to exercise-8/complete/friends/FriendProfile.js diff --git a/exercise-14/friends/FriendProfile.module.css b/exercise-8/complete/friends/FriendProfile.module.css similarity index 100% rename from exercise-14/friends/FriendProfile.module.css rename to exercise-8/complete/friends/FriendProfile.module.css diff --git a/exercise-14/friends/Friends.entry.js b/exercise-8/complete/friends/Friends.entry.js similarity index 100% rename from exercise-14/friends/Friends.entry.js rename to exercise-8/complete/friends/Friends.entry.js diff --git a/exercise-14/friends/Friends.js b/exercise-8/complete/friends/Friends.js similarity index 100% rename from exercise-14/friends/Friends.js rename to exercise-8/complete/friends/Friends.js diff --git a/exercise-14/complete/shared/Card.js b/exercise-8/complete/shared/Card.js similarity index 100% rename from exercise-14/complete/shared/Card.js rename to exercise-8/complete/shared/Card.js diff --git a/exercise-14/shared/Card.module.css b/exercise-8/complete/shared/Card.module.css similarity index 71% rename from exercise-14/shared/Card.module.css rename to exercise-8/complete/shared/Card.module.css index 3aa7162..3265a74 100644 --- a/exercise-14/shared/Card.module.css +++ b/exercise-8/complete/shared/Card.module.css @@ -6,3 +6,6 @@ box-shadow: 0 1px 2px var(--brand-dark); } +.card:hover { + box-shadow: 0 2px 4px var(--brand-dark); +} diff --git a/exercise-14/complete/shared/Page.js b/exercise-8/complete/shared/Page.js similarity index 100% rename from exercise-14/complete/shared/Page.js rename to exercise-8/complete/shared/Page.js diff --git a/exercise-14/complete/shared/Page.module.css b/exercise-8/complete/shared/Page.module.css similarity index 100% rename from exercise-14/complete/shared/Page.module.css rename to exercise-8/complete/shared/Page.module.css diff --git a/exercise-14/complete/data/friends.js b/exercise-8/data/friends.js similarity index 67% rename from exercise-14/complete/data/friends.js rename to exercise-8/data/friends.js index 7ba5db0..5fb4b02 100644 --- a/exercise-14/complete/data/friends.js +++ b/exercise-8/data/friends.js @@ -3,21 +3,18 @@ export default [ id: 1, name: 'Potatoes', image: 'http://placekitten.com/150/150?image=1', - bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.', - colors: [ 'Gray', 'Black' ] + bio: 'This soft little kitten loves to play in window sills, and snuggles like a champion.' }, { id: 2, name: 'Flower', image: 'http://placekitten.com/150/150?image=12', - bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.', - colors: [ 'Gray', 'Black', 'White' ] + bio: 'This teensy little girl is a sweetheart. Her tiny baby sneezes will warm your heart.' }, { id: 3, name: 'Turtle', image: 'http://placekitten.com/150/150?image=15', - bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.', - colors: [ 'Gray', 'White' ] + bio: 'This old pal loves to leap for flying toy mice. Throw anything his way, and he\'ll impress you with some backflips.' }, ]; diff --git a/exercise-8/friend-detail/FriendDetail.entry.js b/exercise-8/friend-detail/FriendDetail.entry.js new file mode 100644 index 0000000..9796960 --- /dev/null +++ b/exercise-8/friend-detail/FriendDetail.entry.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import friends from '../data/friends'; + +import FriendDetail from './FriendDetail'; + +export default function() { + //TODO - how do we get the active friend id??? + const friend = friends[2]; + + return ; +} diff --git a/exercise-8/friend-detail/FriendDetail.js b/exercise-8/friend-detail/FriendDetail.js new file mode 100644 index 0000000..018677e --- /dev/null +++ b/exercise-8/friend-detail/FriendDetail.js @@ -0,0 +1,20 @@ +import React from 'react'; +import Page from '../shared/Page'; +import Card from '../shared/Card'; + +import styles from './FriendDetail.module.css'; + +export default function({ friend }) { + return ( + +
+ +

{friend.name}

+

ID: {friend.id}

+ +

{friend.bio}

+
+
+
+ ); +} diff --git a/exercise-8/friend-detail/FriendDetail.module.css b/exercise-8/friend-detail/FriendDetail.module.css new file mode 100644 index 0000000..3432a3e --- /dev/null +++ b/exercise-8/friend-detail/FriendDetail.module.css @@ -0,0 +1,3 @@ +.friendDetail { + width: 50%; +} diff --git a/exercise-8/friends/FriendProfile.js b/exercise-8/friends/FriendProfile.js new file mode 100644 index 0000000..5f98acd --- /dev/null +++ b/exercise-8/friends/FriendProfile.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import Card from '../shared/Card'; + +import styles from './FriendProfile.module.css'; + +export default function FriendProfile({ name, image }) { + return ( + +
+ {name} +

{name}

+
+
+ ); +} diff --git a/exercise-15/complete/friends/FriendProfile.module.css b/exercise-8/friends/FriendProfile.module.css similarity index 100% rename from exercise-15/complete/friends/FriendProfile.module.css rename to exercise-8/friends/FriendProfile.module.css diff --git a/exercise-8/friends/Friends.entry.js b/exercise-8/friends/Friends.entry.js new file mode 100644 index 0000000..df30e43 --- /dev/null +++ b/exercise-8/friends/Friends.entry.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import myFriends from '../data/friends'; + +import Friends from './Friends'; + +export default function FriendsEntry() { + return +} + diff --git a/exercise-15/complete/friends/Friends.js b/exercise-8/friends/Friends.js similarity index 100% rename from exercise-15/complete/friends/Friends.js rename to exercise-8/friends/Friends.js diff --git a/exercise-14/shared/Card.js b/exercise-8/shared/Card.js similarity index 100% rename from exercise-14/shared/Card.js rename to exercise-8/shared/Card.js diff --git a/exercise-8/shared/Card.module.css b/exercise-8/shared/Card.module.css new file mode 100644 index 0000000..3265a74 --- /dev/null +++ b/exercise-8/shared/Card.module.css @@ -0,0 +1,11 @@ +.card { + min-width: 150px; + background-color: var(--brand-light); + margin: 20px; + padding: 30px; + box-shadow: 0 1px 2px var(--brand-dark); +} + +.card:hover { + box-shadow: 0 2px 4px var(--brand-dark); +} diff --git a/exercise-14/shared/Page.js b/exercise-8/shared/Page.js similarity index 100% rename from exercise-14/shared/Page.js rename to exercise-8/shared/Page.js diff --git a/exercise-14/shared/Page.module.css b/exercise-8/shared/Page.module.css similarity index 100% rename from exercise-14/shared/Page.module.css rename to exercise-8/shared/Page.module.css diff --git a/exercise-9/App.js b/exercise-9/App.js index f0ab35a..3f1b388 100644 --- a/exercise-9/App.js +++ b/exercise-9/App.js @@ -1,18 +1,26 @@ import React from 'react'; -import './App.css'; -import Exercise from './Exercise'; + +import { BrowserRouter, Route } from 'react-router-dom'; + +import Friends from './friends/Friends.entry'; +import FriendDetail from './friend-detail/FriendDetail.entry'; + +import styles from './App.module.css'; function App() { return ( -
-
-

Exercise 9

-

Defining PropTypes

-
-
- + +
+
+

Exercise 12

+

Managing Component State

+
+
+ + +
-
+ ); } diff --git a/exercise-9/App.module.css b/exercise-9/App.module.css new file mode 100644 index 0000000..3688af6 --- /dev/null +++ b/exercise-9/App.module.css @@ -0,0 +1,22 @@ +.app { + text-align: center; + + --brand-dark: blueviolet; + --brand-light: ghostwhite; +} + +.appHeader { + background-color: var(--brand-dark); + height: 100px; + padding: 20px; + color: white; +} + +.emphasize { + text-decoration: underline; +} + +.appTitle { + font-family: 'Pacifico', cursive; + line-height: 0.5em; +} diff --git a/exercise-9/README.md b/exercise-9/README.md index c20856b..283fa73 100644 --- a/exercise-9/README.md +++ b/exercise-9/README.md @@ -1,95 +1,148 @@ # Exercise 9 -## Defining PropTypes -PropTypes are a way to validate props passed into a component. If a propType is not met, you'll get a warning in your browser console. This is a nice way to make sure you're passing around the data your components need. +## Managing Component State -This exercise will introduce you to working with PropTypes. +In this exercise, we'll use component state to flip an information card back-and-forth on our FriendDetail page. 👉 Start the app for Exercise 9 In a console window, pointed at the root of this project, run `npm run start-exercise-9`. -This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 9: Defining PropTypes", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 9: Managing Component State", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. -👉 Open Exercise.js +### FriendFlipper: A New Component -All of your work for this exercise will take place in Exercise.js. +We've redesigned our FriendDetail page a bit. It references a new child component - the ``. -### Defining PropTypes +The `` component will flip an information card for the user. The front is an image of our friend; on the back will be some details, like their ID & colors. The flipper isn't functional yet - but it has all the styles & content built out for it. -In this exercise, all of our components are stateless functional components. This means that we need to attach `propTypes` after the function is declared, like below: +Your responsibility will be to utilize React component state to toggle the visible side of the flipper. + +### Initializing State + +The first thing we need to do with a stateful component is initialize the state. + +In this case, we'll want our initial state to be such that the card is not flipped yet. We will use a boolean state property named `flipped` to manage this. + +Remember that when initializing state, we can do it a couple ways: + +1. Constructor initialization + +This method utilizes the class constructor, and looks something like this: + +```jsx +export default class Component extends React.Component { + constructor(props) { + super(props); // This calls the base constructor, with the passed-in props. + + this.state = { + // set initial state properties here + }; + } + // ... +} +``` + +2. Class property initialization + +This method utilizes a newer feature of JavaScript - class properties - and looks something like this: ```jsx -function MyComponent() { - return
...
; +export default class Component extends React.Component { + state = { + myValue: 0, // or whatever default you want to set it to + }; + + // ... } +``` -MyComponent.propTypes = { - fieldOne: PropTypes.string.isRequired, - fieldTwo: PropTypes.number.isRequired, -}; +👉 Initialize the state in `friend-detail/FriendFlipper.js` so that the `flipped` property is defaulted to `false`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#initialization). + +### Adding An Event Handler + +We need to handle the event that a user "flips" the information card. + +Recall that we handle an event in a stateful component with a class property assigned to a fat-arrow method that calls `setState`. This looks something like this: + +```jsx +export default class Component extends React.Component { + myEventHandler = () => { + this.setState({ + myValue: 1, // or whatever value you want to set it to + }); + }; + // ... +} ``` -Any defined propType field can be marked as required by attaching `.isRequired` to the end of it, like the example above. +If we want to update the state based on the current state of the component, we can use the alternate override for `setState`, like this: -The `PropTypes` object (imported from the `prop-types` dependency) includes a handful of types. Following are a few. +```jsx +export default class Component extends React.Component { + myEventHandler = () => { + this.setState(prevProps => { + return { + myValue: prevProps.myValue + 1, // or whatever value you want to set it to + }; + }); + }; + // ... +} +``` + +👉 Add an event handler to `friend-detail/FriendFlipper.js` that updates the `flipped` state property to the opposite of the current value of `flipped`. -* `array` -* `bool` -* `number` -* `string` -* `object`: Validates that the prop is any object -* `shape`: Validates that the prop is an object of a specified shape +If you get stuck, [see a possible solution here](./SOLUTIONS.md#event-handler). -👉 Define `propTypes` for the `FriendProfile` component. +### Conditionally flipping the card based on state -The props passed into the `FriendProfile` component are: +Once our event handler changes the state, we'll need our `render` function to flip the card based on the `flipped` state property. -* `name`: this is a string, and it is required. -* `image`: this is a string, and it is required. +We can do this by conditionally calling `renderFront()` or `renderBack()` in our `render` function. -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-proptypes). +👉 Conditionally call `renderFront()` or `renderBack()` in `friend-detail/FriendFlipper.js`, based on the value of `this.state.flipped`. -👉 Define `propTypes` for the `Friends` component. +If `this.state.flipped` is true, `renderBack()` should be called. If `this.state.flipped` is false, `renderFront()` should be called. -The props passed into the `Friends` component are: +You can use ternaries to call the appropriate `render___()` function, or call a function that uses an if/else. -* `friends`: this is an array, and it is required. +If you get stuck, [see a possible solution here](./SOLUTIONS.md#conditional-render). -If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-array). +### Tying it all together - connecting the buttons to the event handler -### Specifying the shape of an array +We have one last step to hook up our state management. When the buttons on the information card are clicked, they need to call our event handler. -When we define a propType of type array, we can also specify the shape of items in the array. Following is an example of a component that requires an array named `people`. Each element of the `people` array must contain two properties: `firstName` and `lastName`. +Calling event handlers from a button click looks like this: ```jsx -MyComponent.propTypes = { - people: PropTypes.arrayOf( - PropTypes.shape({ - firstName: PropTypes.string.isRequired, - lastName: PropTypes.string.isRequired, - }) - ) +class MyComponent extends React.Component { + myEventHandler = () => { + // ... + }; + + render() { + // ... + +
+
+ ); + } + + renderBack() { + const { friend } = this.props; + return ( +
+
+ {friend.image} +
+

+ ID: + {friend.id} +

+

Colors:

+
    + {friend.colors.map(color => ( +
  • {color}
  • + ))} +
+
+ +
+
+ ); + } +} diff --git a/exercise-16/friend-detail/FriendFlipper.module.css b/exercise-9/friend-detail/FriendFlipper.module.css similarity index 80% rename from exercise-16/friend-detail/FriendFlipper.module.css rename to exercise-9/friend-detail/FriendFlipper.module.css index 8135352..da93169 100644 --- a/exercise-16/friend-detail/FriendFlipper.module.css +++ b/exercise-9/friend-detail/FriendFlipper.module.css @@ -9,7 +9,6 @@ .flipper { position: relative; - background-color: gray; } /* hide back of pane during swap */ @@ -23,21 +22,14 @@ box-shadow: 0 1px 2px; } -/* front pane, placed above back */ - .flipperNav { + background: var(--brand-dark); border: none; padding: 5px 10px; color: white; text-decoration: underline; cursor: pointer; } -.flipperNav.green { - background: teal; -} -.flipperNav.purple { - background: blueviolet; -} .flipperNav:focus { opacity: 0.8; outline: none; @@ -49,17 +41,12 @@ } .backContents { font-size: 0.8rem; + background-color: var(--brand-dark); color: white; text-align: left; height: calc(100% - 10px); padding: 5px; } -.backContents.green { - background: teal; -} -.backContents.purple { - background: blueviolet; -} .backContents .flipperNav { position: absolute; bottom: 5px; diff --git a/exercise-15/complete/friends/FriendProfile.js b/exercise-9/friends/FriendProfile.js similarity index 100% rename from exercise-15/complete/friends/FriendProfile.js rename to exercise-9/friends/FriendProfile.js diff --git a/exercise-15/friends/FriendProfile.module.css b/exercise-9/friends/FriendProfile.module.css similarity index 100% rename from exercise-15/friends/FriendProfile.module.css rename to exercise-9/friends/FriendProfile.module.css diff --git a/exercise-9/friends/Friends.entry.js b/exercise-9/friends/Friends.entry.js new file mode 100644 index 0000000..df30e43 --- /dev/null +++ b/exercise-9/friends/Friends.entry.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import myFriends from '../data/friends'; + +import Friends from './Friends'; + +export default function FriendsEntry() { + return +} + diff --git a/exercise-15/friends/Friends.js b/exercise-9/friends/Friends.js similarity index 100% rename from exercise-15/friends/Friends.js rename to exercise-9/friends/Friends.js diff --git a/exercise-9/shared/Card.js b/exercise-9/shared/Card.js new file mode 100644 index 0000000..5268294 --- /dev/null +++ b/exercise-9/shared/Card.js @@ -0,0 +1,7 @@ +import React from 'react'; + +import styles from './Card.module.css'; + +export default function Card({ children }) { + return
{children}
; +} diff --git a/exercise-14/complete/shared/Card.module.css b/exercise-9/shared/Card.module.css similarity index 100% rename from exercise-14/complete/shared/Card.module.css rename to exercise-9/shared/Card.module.css diff --git a/exercise-15/shared/Page.js b/exercise-9/shared/Page.js similarity index 59% rename from exercise-15/shared/Page.js rename to exercise-9/shared/Page.js index c562e9a..ba4950b 100644 --- a/exercise-15/shared/Page.js +++ b/exercise-9/shared/Page.js @@ -1,12 +1,10 @@ import React from 'react'; -import classNames from 'classnames'; -import theme from '../theme/static'; import styles from './Page.module.css'; export default function Page({ children }) { return ( -
+
{children}
); diff --git a/exercise-16/shared/Page.module.css b/exercise-9/shared/Page.module.css similarity index 62% rename from exercise-16/shared/Page.module.css rename to exercise-9/shared/Page.module.css index 5a58fd6..0681578 100644 --- a/exercise-16/shared/Page.module.css +++ b/exercise-9/shared/Page.module.css @@ -2,14 +2,9 @@ display: flex; flex-direction: row; justify-content: center; + background: repeating-linear-gradient(to top, var(--brand-light), var(--brand-dark)); height: calc(100vh - 140px); } -.page.green { - background: repeating-linear-gradient(to top, mintcream, teal); -} -.page.purple { - background: repeating-linear-gradient(to top, ghostwhite, blueviolet); -} .content{ background-color: white; diff --git a/exercise-16/App.js b/exercise-xx-hooks/App.js similarity index 100% rename from exercise-16/App.js rename to exercise-xx-hooks/App.js diff --git a/exercise-15/complete/App.module.css b/exercise-xx-hooks/App.module.css similarity index 100% rename from exercise-15/complete/App.module.css rename to exercise-xx-hooks/App.module.css diff --git a/exercise-16/Header.js b/exercise-xx-hooks/Header.js similarity index 100% rename from exercise-16/Header.js rename to exercise-xx-hooks/Header.js diff --git a/exercise-16/Header.module.css b/exercise-xx-hooks/Header.module.css similarity index 100% rename from exercise-16/Header.module.css rename to exercise-xx-hooks/Header.module.css diff --git a/exercise-xx-hooks/README.md b/exercise-xx-hooks/README.md new file mode 100644 index 0000000..c2d3dff --- /dev/null +++ b/exercise-xx-hooks/README.md @@ -0,0 +1,232 @@ +# Exercise 16 + +## React Hooks + +This exercise will give you hands-on experience converting code to use the upcoming React hooks API. Hooks will simplify how we use several React features that we've already seen. + +👉 Start the app for Exercise 16 + +In a console window, pointed at the root of this project, run `npm run start-exercise-16`. + +This will open a browser window pointed at localhost:3000, showing a web app titled "Exercise 16: Hooks", our three adorable kitten friends, and a theme switcher that toggles between purple and green themes. If it doesn't, ask your neighbor for assistance or raise your hand. + +### New Components: FriendFlipperBack and FriendFlipperFront + +We've refactored the FriendFlipper component, extracting the `renderBack()` and `renderFront()` functions into `FriendFlipperBack` and `FriendFlipperFront` components. This will help us focus on the changes we're making. + +--- + +### Rethinking State Management: `useState()` + +The `useState` hook is a way to manage state in a functional React component without converting it to a class. + +Here's an example of a stateful component written as a class (**not** using the `useState` hook): + +```jsx +import React from 'react'; + +class MyCheckBox extends React.Component { + state = { + checked: false, + }; + + handleChanged = e => { + this.setState({ checked: e.target.checked }); + }; + + render() { + return ( + + ); + } +} +``` + +And here is that same component written as a stateful function, with the `useState` hook: + +```jsx +import React, { useState } from 'react'; + +function MyCheckBox() { + const [checked, setChecked] = useState(false); + + return ( + setChecked(!checked)} + /> + ); +} +``` + +There are 4 steps to using the `useState` hook in a component: + +1. If the component is a class, convert it to a function. We practiced this in [exercise 7](../exercise-7/README.md). + +2. Import the `useState` function as a named import, from the 'react' dependency. (`import React, { useState } from 'react';`) + +3. Call `useState` at the beginning of the function. + + a. Pass the default value as the lone argument to `useState`. In the example above, the default value is `false`. + + b. An array is returned by `useState`. The first item in the array represents the current value of the state item; the second item is a function we can call to change the value of the state item. In the example above, we are destructuring the returned array into `checked` and `setChecked` variables. + +4. Use the state variables from 3b (`checked` and `setChecked`) in the rendered component. + +👉 Convert `friend-detail/FriendFlipper.js` from a stateful class component to a stateful function component, using the `useState` hook. + +Use the example & steps above as a guide. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendflipper-usestate). + +--- + +### Rethinking Lifecycle Events: `useEffect()` + +The `useEffect` hook allows you to tap into React lifecycle events in a functional React component, without converting it to a class. + +Here's an example of a class component that utilizes lifecycle events (**not** using the `useEffect` hook): + +```jsx +import React from 'react'; + +class Chat extends React.Component { + componentDidMount () { + socket.emit('join', { id: this.props.friendId }); + } + + componentDidUpdate(nextProps) { + if (nextProps.friendId !== this.props.friendId) { + socket.emit('leave', { id: this.props.friendId }); + socket.emit('join', { id: nextProps.friendId }); + } + } + + componentWillUnmount () { + socket.emit('leave', { id: this.props.friendId }); + } + + render() { ... } +} +``` + +This component joins a socket when the component mounts, leaves the original socket and joins a new one when the `friendId` prop changes, and leaves the socket when the component unmounts. + +Here is the same component written as a functional component that uses the `useEffect` hook: + +```jsx +import React, { useEffect } from 'react'; + +function Chat(props) { + + useEffect(() => { + socket.emit('join', { id: props.friendId }); + + return () => { + socket.emit('leave', { id: props.friendId }); + } + + }, [ props.friendId ]) + + return ( ... ) +} +``` + +There are 3 steps to using the `useEffect` hook in a component: + +1. If the component is a class, convert it to a function. We practiced this in [exercise 7](../exercise-7/README.md). + +2. Import the `useEffect` function as a named import, from the 'react' dependency. (`import React, { useEffect } from 'react';`) + +3. Call `useEffect` at the beginning of the function. + + a. The first argument to `useEffect` is a function that should execute when the component mounts, or when props change that require the effect to re-run. In the example above, this function joins a socket based on `props.friendId`. + + b. Not all effects require "cleanup" code - but if they do, this is accomplished by the function in 3a returning another anonymous function. This returned function will execute when the component unmounts, or when props change that require the effect to re-run. In our example above, this "cleanup" function will leave a socket based on the friendId. + + c. The second argument to `useEffect` is an array. This array will contain all props which, when their value changes, would require the effect to re-run. In the example above, we pass `[ props.friendId ]` - this means that the effect will re-run whenever the value of the `friendId` prop changes. + +👉 Convert `friends/Friends.entry.js` from a class component to a function component, using the `useEffect` hook. + +Use the example & steps above as a guide. + +Notice that this component manages state, in addition to using the `componentDidMount` lifecycle method. You'll want to convert the stateful portions to use `useState`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends.entry-useeffect). + +--- + +### Rethinking Context: `useContext()` + +The `useContext` hook eliminates the need for the "function as a child" pattern when consuming a context in a component. "Function as a child", similar to the "render props" pattern, is a useful pattern in React apps, but it can also be confusing. You can read more about these patterns [here](https://reactjs.org/docs/render-props.html). + +Here's an example of a component that consumes a context without the `useContext` hook. Notice how the child of `` is a function: + +```jsx +import React from 'react'; +import CurrentUserContext from './context'; + +export default function CurrentUser() { + return ( + + {({ currentUser, handleUserLogout }) => ( +
+ {currentUser.name} + +
+ )} +
+ ); +} +``` + +And here is the same component, using the `useContext` hook: + +```jsx +import React, { useContext } from 'react'; + +import CurrentUserContext from './context'; + +export default function CurrentUser() { + const { currentUser, handleUserLogout } = useContext(CurrentUserContext); + + return ( +
+ {currentUser.name} + +
+ ); +} +``` + +There are 3 steps to using the `useContext` hook in a component: + +1. Import the `useContext` function as a named import, from the 'react' dependency. (`import React, { useContext } from 'react';`) + +2. Call `useContext` at the beginning of the function. + + a. Pass the associated context as the lone argument to `useContext`. In the example above, that's the `CurrentUserContext` context. + + b. The "value" of the context is returned by `useContext`. This will have been defined by you when you create the context. In our example, it is an object containing `currentUser` and `handleUserLogout` properties. + +3. Use the context value from 2b (an object with `currentUser` and `handleUserLogout`) in the rendered component. + +👉 Convert `theme/Switcher.js` to use the `useContext` hook. + +Use the example & steps above as a guide. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#switcher-usecontext). + +### Extra Credit + +The following components could also be converted to use hooks: + +- friend-detail/FriendDetail.entry.js (`useState`, `useEffect`) +- friend-detail/FriendFlipperBack.js (`useContext`) +- friend-detail/FriendFlipperFront.js (`useContext`) +- theme/Provider.js (`useState`) diff --git a/exercise-16/SOLUTIONS.md b/exercise-xx-hooks/SOLUTIONS.md similarity index 100% rename from exercise-16/SOLUTIONS.md rename to exercise-xx-hooks/SOLUTIONS.md diff --git a/exercise-16/complete/App.js b/exercise-xx-hooks/complete/App.js similarity index 100% rename from exercise-16/complete/App.js rename to exercise-xx-hooks/complete/App.js diff --git a/exercise-16/App.module.css b/exercise-xx-hooks/complete/App.module.css similarity index 100% rename from exercise-16/App.module.css rename to exercise-xx-hooks/complete/App.module.css diff --git a/exercise-16/complete/Header.js b/exercise-xx-hooks/complete/Header.js similarity index 100% rename from exercise-16/complete/Header.js rename to exercise-xx-hooks/complete/Header.js diff --git a/exercise-16/complete/Header.module.css b/exercise-xx-hooks/complete/Header.module.css similarity index 100% rename from exercise-16/complete/Header.module.css rename to exercise-xx-hooks/complete/Header.module.css diff --git a/exercise-16/complete/friend-detail/FriendDetail.entry.js b/exercise-xx-hooks/complete/friend-detail/FriendDetail.entry.js similarity index 100% rename from exercise-16/complete/friend-detail/FriendDetail.entry.js rename to exercise-xx-hooks/complete/friend-detail/FriendDetail.entry.js diff --git a/exercise-16/complete/friend-detail/FriendDetail.js b/exercise-xx-hooks/complete/friend-detail/FriendDetail.js similarity index 100% rename from exercise-16/complete/friend-detail/FriendDetail.js rename to exercise-xx-hooks/complete/friend-detail/FriendDetail.js diff --git a/exercise-15/complete/friend-detail/FriendDetail.module.css b/exercise-xx-hooks/complete/friend-detail/FriendDetail.module.css similarity index 100% rename from exercise-15/complete/friend-detail/FriendDetail.module.css rename to exercise-xx-hooks/complete/friend-detail/FriendDetail.module.css diff --git a/exercise-16/complete/friend-detail/FriendFlipper.js b/exercise-xx-hooks/complete/friend-detail/FriendFlipper.js similarity index 100% rename from exercise-16/complete/friend-detail/FriendFlipper.js rename to exercise-xx-hooks/complete/friend-detail/FriendFlipper.js diff --git a/exercise-15/complete/friend-detail/FriendFlipper.module.css b/exercise-xx-hooks/complete/friend-detail/FriendFlipper.module.css similarity index 100% rename from exercise-15/complete/friend-detail/FriendFlipper.module.css rename to exercise-xx-hooks/complete/friend-detail/FriendFlipper.module.css diff --git a/exercise-16/complete/friend-detail/FriendFlipperBack.js b/exercise-xx-hooks/complete/friend-detail/FriendFlipperBack.js similarity index 100% rename from exercise-16/complete/friend-detail/FriendFlipperBack.js rename to exercise-xx-hooks/complete/friend-detail/FriendFlipperBack.js diff --git a/exercise-16/complete/friend-detail/FriendFlipperFront.js b/exercise-xx-hooks/complete/friend-detail/FriendFlipperFront.js similarity index 100% rename from exercise-16/complete/friend-detail/FriendFlipperFront.js rename to exercise-xx-hooks/complete/friend-detail/FriendFlipperFront.js diff --git a/exercise-16/complete/friend-detail/get-friend-from-api.js b/exercise-xx-hooks/complete/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-16/complete/friend-detail/get-friend-from-api.js rename to exercise-xx-hooks/complete/friend-detail/get-friend-from-api.js diff --git a/exercise-15/friends/FriendProfile.js b/exercise-xx-hooks/complete/friends/FriendProfile.js similarity index 100% rename from exercise-15/friends/FriendProfile.js rename to exercise-xx-hooks/complete/friends/FriendProfile.js diff --git a/exercise-16/complete/friends/FriendProfile.module.css b/exercise-xx-hooks/complete/friends/FriendProfile.module.css similarity index 100% rename from exercise-16/complete/friends/FriendProfile.module.css rename to exercise-xx-hooks/complete/friends/FriendProfile.module.css diff --git a/exercise-16/complete/friends/Friends.entry.js b/exercise-xx-hooks/complete/friends/Friends.entry.js similarity index 100% rename from exercise-16/complete/friends/Friends.entry.js rename to exercise-xx-hooks/complete/friends/Friends.entry.js diff --git a/exercise-16/complete/friends/Friends.js b/exercise-xx-hooks/complete/friends/Friends.js similarity index 100% rename from exercise-16/complete/friends/Friends.js rename to exercise-xx-hooks/complete/friends/Friends.js diff --git a/exercise-16/complete/friends/get-friends-from-api.js b/exercise-xx-hooks/complete/friends/get-friends-from-api.js similarity index 100% rename from exercise-16/complete/friends/get-friends-from-api.js rename to exercise-xx-hooks/complete/friends/get-friends-from-api.js diff --git a/exercise-16/complete/shared/Card.js b/exercise-xx-hooks/complete/shared/Card.js similarity index 100% rename from exercise-16/complete/shared/Card.js rename to exercise-xx-hooks/complete/shared/Card.js diff --git a/exercise-15/shared/Card.module.css b/exercise-xx-hooks/complete/shared/Card.module.css similarity index 100% rename from exercise-15/shared/Card.module.css rename to exercise-xx-hooks/complete/shared/Card.module.css diff --git a/exercise-16/complete/shared/Page.js b/exercise-xx-hooks/complete/shared/Page.js similarity index 100% rename from exercise-16/complete/shared/Page.js rename to exercise-xx-hooks/complete/shared/Page.js diff --git a/exercise-15/shared/Page.module.css b/exercise-xx-hooks/complete/shared/Page.module.css similarity index 100% rename from exercise-15/shared/Page.module.css rename to exercise-xx-hooks/complete/shared/Page.module.css diff --git a/exercise-16/complete/theme/Provider.js b/exercise-xx-hooks/complete/theme/Provider.js similarity index 100% rename from exercise-16/complete/theme/Provider.js rename to exercise-xx-hooks/complete/theme/Provider.js diff --git a/exercise-16/complete/theme/Switcher.js b/exercise-xx-hooks/complete/theme/Switcher.js similarity index 100% rename from exercise-16/complete/theme/Switcher.js rename to exercise-xx-hooks/complete/theme/Switcher.js diff --git a/exercise-16/complete/theme/Switcher.module.css b/exercise-xx-hooks/complete/theme/Switcher.module.css similarity index 100% rename from exercise-16/complete/theme/Switcher.module.css rename to exercise-xx-hooks/complete/theme/Switcher.module.css diff --git a/exercise-16/complete/theme/context.js b/exercise-xx-hooks/complete/theme/context.js similarity index 100% rename from exercise-16/complete/theme/context.js rename to exercise-xx-hooks/complete/theme/context.js diff --git a/exercise-16/complete/theme/static.js b/exercise-xx-hooks/complete/theme/static.js similarity index 100% rename from exercise-16/complete/theme/static.js rename to exercise-xx-hooks/complete/theme/static.js diff --git a/exercise-16/data/db.json b/exercise-xx-hooks/data/db.json similarity index 100% rename from exercise-16/data/db.json rename to exercise-xx-hooks/data/db.json diff --git a/exercise-15/friend-detail/FriendDetail.entry.js b/exercise-xx-hooks/friend-detail/FriendDetail.entry.js similarity index 100% rename from exercise-15/friend-detail/FriendDetail.entry.js rename to exercise-xx-hooks/friend-detail/FriendDetail.entry.js diff --git a/exercise-16/friend-detail/FriendDetail.js b/exercise-xx-hooks/friend-detail/FriendDetail.js similarity index 100% rename from exercise-16/friend-detail/FriendDetail.js rename to exercise-xx-hooks/friend-detail/FriendDetail.js diff --git a/exercise-15/friend-detail/FriendDetail.module.css b/exercise-xx-hooks/friend-detail/FriendDetail.module.css similarity index 100% rename from exercise-15/friend-detail/FriendDetail.module.css rename to exercise-xx-hooks/friend-detail/FriendDetail.module.css diff --git a/exercise-16/friend-detail/FriendFlipper.js b/exercise-xx-hooks/friend-detail/FriendFlipper.js similarity index 100% rename from exercise-16/friend-detail/FriendFlipper.js rename to exercise-xx-hooks/friend-detail/FriendFlipper.js diff --git a/exercise-15/friend-detail/FriendFlipper.module.css b/exercise-xx-hooks/friend-detail/FriendFlipper.module.css similarity index 100% rename from exercise-15/friend-detail/FriendFlipper.module.css rename to exercise-xx-hooks/friend-detail/FriendFlipper.module.css diff --git a/exercise-16/friend-detail/FriendFlipperBack.js b/exercise-xx-hooks/friend-detail/FriendFlipperBack.js similarity index 100% rename from exercise-16/friend-detail/FriendFlipperBack.js rename to exercise-xx-hooks/friend-detail/FriendFlipperBack.js diff --git a/exercise-16/friend-detail/FriendFlipperFront.js b/exercise-xx-hooks/friend-detail/FriendFlipperFront.js similarity index 100% rename from exercise-16/friend-detail/FriendFlipperFront.js rename to exercise-xx-hooks/friend-detail/FriendFlipperFront.js diff --git a/exercise-16/friend-detail/get-friend-from-api.js b/exercise-xx-hooks/friend-detail/get-friend-from-api.js similarity index 100% rename from exercise-16/friend-detail/get-friend-from-api.js rename to exercise-xx-hooks/friend-detail/get-friend-from-api.js diff --git a/exercise-16/complete/friends/FriendProfile.js b/exercise-xx-hooks/friends/FriendProfile.js similarity index 100% rename from exercise-16/complete/friends/FriendProfile.js rename to exercise-xx-hooks/friends/FriendProfile.js diff --git a/exercise-16/friends/FriendProfile.module.css b/exercise-xx-hooks/friends/FriendProfile.module.css similarity index 100% rename from exercise-16/friends/FriendProfile.module.css rename to exercise-xx-hooks/friends/FriendProfile.module.css diff --git a/exercise-15/complete/friends/Friends.entry.js b/exercise-xx-hooks/friends/Friends.entry.js similarity index 100% rename from exercise-15/complete/friends/Friends.entry.js rename to exercise-xx-hooks/friends/Friends.entry.js diff --git a/exercise-16/friends/Friends.js b/exercise-xx-hooks/friends/Friends.js similarity index 100% rename from exercise-16/friends/Friends.js rename to exercise-xx-hooks/friends/Friends.js diff --git a/exercise-16/friends/get-friends-from-api.js b/exercise-xx-hooks/friends/get-friends-from-api.js similarity index 100% rename from exercise-16/friends/get-friends-from-api.js rename to exercise-xx-hooks/friends/get-friends-from-api.js diff --git a/exercise-15/index.css b/exercise-xx-hooks/index.css similarity index 100% rename from exercise-15/index.css rename to exercise-xx-hooks/index.css diff --git a/exercise-16/index.js b/exercise-xx-hooks/index.js similarity index 100% rename from exercise-16/index.js rename to exercise-xx-hooks/index.js diff --git a/exercise-16/shared/Card.js b/exercise-xx-hooks/shared/Card.js similarity index 100% rename from exercise-16/shared/Card.js rename to exercise-xx-hooks/shared/Card.js diff --git a/exercise-16/complete/shared/Card.module.css b/exercise-xx-hooks/shared/Card.module.css similarity index 100% rename from exercise-16/complete/shared/Card.module.css rename to exercise-xx-hooks/shared/Card.module.css diff --git a/exercise-16/shared/Page.js b/exercise-xx-hooks/shared/Page.js similarity index 100% rename from exercise-16/shared/Page.js rename to exercise-xx-hooks/shared/Page.js diff --git a/exercise-16/complete/shared/Page.module.css b/exercise-xx-hooks/shared/Page.module.css similarity index 100% rename from exercise-16/complete/shared/Page.module.css rename to exercise-xx-hooks/shared/Page.module.css diff --git a/exercise-16/theme/Provider.js b/exercise-xx-hooks/theme/Provider.js similarity index 100% rename from exercise-16/theme/Provider.js rename to exercise-xx-hooks/theme/Provider.js diff --git a/exercise-16/theme/Switcher.js b/exercise-xx-hooks/theme/Switcher.js similarity index 100% rename from exercise-16/theme/Switcher.js rename to exercise-xx-hooks/theme/Switcher.js diff --git a/exercise-16/theme/Switcher.module.css b/exercise-xx-hooks/theme/Switcher.module.css similarity index 100% rename from exercise-16/theme/Switcher.module.css rename to exercise-xx-hooks/theme/Switcher.module.css diff --git a/exercise-16/theme/context.js b/exercise-xx-hooks/theme/context.js similarity index 100% rename from exercise-16/theme/context.js rename to exercise-xx-hooks/theme/context.js diff --git a/exercise-16/theme/static.js b/exercise-xx-hooks/theme/static.js similarity index 100% rename from exercise-16/theme/static.js rename to exercise-xx-hooks/theme/static.js diff --git a/exercise-9/App.css b/exercise-xx-prop-types/App.css similarity index 100% rename from exercise-9/App.css rename to exercise-xx-prop-types/App.css diff --git a/exercise-7/App.js b/exercise-xx-prop-types/App.js similarity index 73% rename from exercise-7/App.js rename to exercise-xx-prop-types/App.js index 9f63621..f0ab35a 100644 --- a/exercise-7/App.js +++ b/exercise-xx-prop-types/App.js @@ -6,8 +6,8 @@ function App() { return (
-

Exercise 7

-

Convert a Component

+

Exercise 9

+

Defining PropTypes

diff --git a/exercise-9/Exercise.js b/exercise-xx-prop-types/Exercise.js similarity index 100% rename from exercise-9/Exercise.js rename to exercise-xx-prop-types/Exercise.js diff --git a/exercise-xx-prop-types/README.md b/exercise-xx-prop-types/README.md new file mode 100644 index 0000000..c20856b --- /dev/null +++ b/exercise-xx-prop-types/README.md @@ -0,0 +1,95 @@ +# Exercise 9 +## Defining PropTypes + +PropTypes are a way to validate props passed into a component. If a propType is not met, you'll get a warning in your browser console. This is a nice way to make sure you're passing around the data your components need. + +This exercise will introduce you to working with PropTypes. + +👉 Start the app for Exercise 9 + +In a console window, pointed at the root of this project, run `npm run start-exercise-9`. + +This should open a browser window pointed at localhost:3000, showing a web app titled "Exercise 9: Defining PropTypes", and our three adorable kitten friends. If it doesn't, ask your neighbor for assistance or raise your hand. + +👉 Open Exercise.js + +All of your work for this exercise will take place in Exercise.js. + +### Defining PropTypes + +In this exercise, all of our components are stateless functional components. This means that we need to attach `propTypes` after the function is declared, like below: + +```jsx +function MyComponent() { + return
...
; +} + +MyComponent.propTypes = { + fieldOne: PropTypes.string.isRequired, + fieldTwo: PropTypes.number.isRequired, +}; +``` + +Any defined propType field can be marked as required by attaching `.isRequired` to the end of it, like the example above. + +The `PropTypes` object (imported from the `prop-types` dependency) includes a handful of types. Following are a few. + +* `array` +* `bool` +* `number` +* `string` +* `object`: Validates that the prop is any object +* `shape`: Validates that the prop is an object of a specified shape + +👉 Define `propTypes` for the `FriendProfile` component. + +The props passed into the `FriendProfile` component are: + +* `name`: this is a string, and it is required. +* `image`: this is a string, and it is required. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friendprofile-proptypes). + +👉 Define `propTypes` for the `Friends` component. + +The props passed into the `Friends` component are: + +* `friends`: this is an array, and it is required. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-array). + +### Specifying the shape of an array + +When we define a propType of type array, we can also specify the shape of items in the array. Following is an example of a component that requires an array named `people`. Each element of the `people` array must contain two properties: `firstName` and `lastName`. + +```jsx +MyComponent.propTypes = { + people: PropTypes.arrayOf( + PropTypes.shape({ + firstName: PropTypes.string.isRequired, + lastName: PropTypes.string.isRequired, + }) + ) +} +``` +👉 Specify the shape of elements required in the `friends` array passed into the `Friends` component, using `PropTypes.arrayOf` and `PropTypes.shape`. + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#friends-arrayof-shape). + +👉 Remove a required property from the `myFriends` array. + +You choose which one. After removing the property, check your browser console to see what happens when a required prop isn't passed into a component. + +You should see an error like this: + +``` +index.js:2178 Warning: Failed prop type: The prop `image` is marked as required in `FriendProfile`, but its value is `undefined`. +``` + +If you get stuck, [see a possible solution here](./SOLUTIONS.md#removed-property). + +### Extra Credit + +* Read about [other the different types you can validate with PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html). + +* Read about [static typechecking, and why it might be better than using PropTypes](https://reactjs.org/docs/static-type-checking.html). \ No newline at end of file diff --git a/exercise-xx-prop-types/SOLUTIONS.md b/exercise-xx-prop-types/SOLUTIONS.md new file mode 100644 index 0000000..b7f09d8 --- /dev/null +++ b/exercise-xx-prop-types/SOLUTIONS.md @@ -0,0 +1,54 @@ +# Possible Solutions + +## FriendProfile PropTypes + +```javascript +FriendProfile.propTypes = { + name: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, +}; +``` + +## Friends array + +```javascript +Friends.propTypes = { + friends: PropTypes.array.isRequired, +}; +``` + +## Friends arrayOf shape + +```javascript +Friends.propTypes = { + friends: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, + }) + ).isRequired, +}; +``` + +## Removed Property + +```javascript +const myFriends = [ + { + id: 1, + name: 'Potatoes', + image: 'http://placekitten.com/150/150?image=1', + }, + { + id: 2, + name: 'Flower', + image: 'http://placekitten.com/150/150?image=12', + }, + { + id: 3, + name: 'Turtle', + // image is missing! + }, +]; +``` diff --git a/exercise-9/complete/Exercise.js b/exercise-xx-prop-types/complete/Exercise.js similarity index 100% rename from exercise-9/complete/Exercise.js rename to exercise-xx-prop-types/complete/Exercise.js diff --git a/exercise-16/index.css b/exercise-xx-prop-types/index.css similarity index 100% rename from exercise-16/index.css rename to exercise-xx-prop-types/index.css diff --git a/exercise-10/index.js b/exercise-xx-prop-types/index.js similarity index 51% rename from exercise-10/index.js rename to exercise-xx-prop-types/index.js index 396e44c..395b749 100644 --- a/exercise-10/index.js +++ b/exercise-xx-prop-types/index.js @@ -1,8 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import App from './1-component-css-files/App'; -// import App from './2-css-modules/App'; -// import App from './3-styled-components/App'; +import App from './App'; ReactDOM.render(, document.getElementById('root'));