Time spent viewing profile: {secondsElapsed} seconds.
11 |
12 |
13 | );
14 | };
15 |
16 | export default Profile;
17 |
--------------------------------------------------------------------------------
/src/hooks/useSecondsElapsed.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | /**
4 | * This custom hook returns the number of seconds that have elapsed since the component was mounted.
5 | * The timer resets when the key changes.
6 | * @param key The key to use for resetting the timer
7 | * @returns The number of seconds that have elapsed since the component was mounted
8 | */
9 | export const useSecondsElapsed = (key: string) => {
10 | const [secondsElapsed, setSecondsElapsed] = useState(0);
11 |
12 | useEffect(() => {
13 | // Reset the timer when the key changes
14 | setSecondsElapsed(0);
15 |
16 | setInterval(() => {
17 | setSecondsElapsed((prevTime) => prevTime + 1);
18 | }, 1000);
19 | }, [key]);
20 |
21 | return secondsElapsed;
22 | };
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "f24-advanced-react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.3.1",
14 | "react-dom": "^18.3.1"
15 | },
16 | "devDependencies": {
17 | "@eslint/js": "^9.9.0",
18 | "@types/react": "^18.3.3",
19 | "@types/react-dom": "^18.3.0",
20 | "@vitejs/plugin-react": "^4.3.1",
21 | "eslint": "^9.9.0",
22 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
23 | "eslint-plugin-react-refresh": "^0.4.9",
24 | "typescript": "^5.5.3",
25 | "typescript-eslint": "^8.0.1",
26 | "vite": "^5.4.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/esling.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "react-refresh/only-export-components": [
23 | "warn",
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | }
28 | );
29 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* styles.css */
2 |
3 | /* Reset default styles */
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | }
9 |
10 | body {
11 | font-family: Arial, sans-serif;
12 | background-color: #f2f2f2;
13 | }
14 |
15 | main {
16 | margin-top: 5vh;
17 | text-align: center;
18 | }
19 |
20 | .container {
21 | max-width: 1200px;
22 | margin: 0 auto;
23 | padding: 20px;
24 | }
25 |
26 | h1 {
27 | color: #333;
28 | font-size: 24px;
29 | font-weight: bold;
30 | margin-bottom: 10px;
31 | }
32 |
33 | p {
34 | color: #666;
35 | font-size: 16px;
36 | line-height: 1.5;
37 | }
38 |
39 | .button {
40 | display: inline-block;
41 | padding: 10px 20px;
42 | background-color: #007bff;
43 | color: #fff;
44 | text-decoration: none;
45 | border-radius: 4px;
46 | transition: background-color 0.3s ease;
47 | }
48 |
49 | .button:hover {
50 | background-color: #0056b3;
51 | }
52 |
53 | .profile {
54 | margin-top: 5vh;
55 |
56 | display: flex;
57 | justify-content: center;
58 | align-items: center;
59 | flex-direction: column;
60 | }
61 |
62 | .profile-card {
63 | background-color: #fff;
64 | border-radius: 4px;
65 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
66 | padding: 30px;
67 | margin-bottom: 20px;
68 |
69 | max-width: max(30%, 350px);
70 | min-width: auto;
71 | }
72 |
73 | .profile-card-header {
74 | margin-bottom: 20px;
75 | }
76 |
77 | .profile-card-header img {
78 | border-radius: 4px;
79 | margin-bottom: 20px;
80 |
81 | width: 80px;
82 | height: 80px;
83 | }
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Commands to run for testing on dev environment
2 |
3 | ### Setup Commands
4 |
5 | Use `npm install` to download all of the dependencies needed to run the web app
6 |
7 | Use `npm run dev` to start the application
8 |
9 | ## List of Goals
10 |
11 | There are 3 main open-ended exercises for this workshop. You are encouraged to collaborate with each other, and consult outside resources.
12 |
13 | 1. Prop drilling, context, and state management
14 | 2. Understanding effects
15 | 3. Abstracting logic to custom hooks
16 |
17 | ## Exercises
18 |
19 | ### 1) Prop drilling, context, and state management
20 |
21 | The goal of the first exercise is to clean up the code such that we are not passing down `profileData` into all of our components. I recommend using **React Context** to accomplish this feat. Use the slides as guidance, but you will likely need to research the proper syntax to implement this. You can create new files and components, but do not delete any existing components. You _may_ add new hooks to the exisiting components.
22 |
23 | Deliverable: After this step, `Profile`, `ProfileCard`, `ProfileCardContent`, and `ProfileCardHeader` should have **no props** passed into them. All functionality should remain the same.
24 |
25 | ### 2) Understanding useEffect
26 |
27 | The goal of the first exercise is to identify some of the bad practices of `useEffect` in React code. There are 2 bugs to fix in the code.
28 |
29 | 1. First, open the browser console by right clicking the window and clicking "inspect" or by using your device's shortcut, and then clicking "console" in the menu bar.
30 |
31 | You will notice that when you click "change user" the app logs a few things to the console. It will log a debug statement, that is just for demonstration purposes. It will also print "rendering app" 2 times. If it does not render 2 times, let us know before progressing.
32 |
33 | The goal of this exercise is to only have the app render a single time when changing users, such that "rendering app" only is printed to the console once.
34 |
35 | It is important to note that the app is _not_ running in React strict mode.
36 |
37 | You should only need to modify code in the `App.tsx` file.
38 |
39 | 2. The second task is the fix the broken timer in the `useSecondsElapsed` hook.
40 |
41 | Currently, it does not accurately count time after switching profiles. While it does properly, reset to zero, you'll notice it fails to accuraretely count seconds.
42 |
43 | You should only need to modify code in `useSecondsElapsed.tsx`
44 |
45 | Deliverables:
46 |
47 | 1. Clicking the change user button should only print "rendering app" to the console twice, not four times.
48 | 2. The timer should count seconds accurately
49 |
50 | ### 3) Building your first custom hook
51 |
52 | This task is a bit more open ended. The goal is to create a hook called `useProfileViews` which tracks the total number of views each profile has received in the current session (it lives in memory and will reset when the user reloads the page). If you would like to challenge yourself, feel free to try and make the hook persist across sessions!
53 |
54 | Deliverables: The profile views for that profile should be displayed on each profile. Loading a profile should increment the views for that profile.
55 |
56 | ## Submission
57 |
58 | Please create a pull request to the repository on GitHub. Mention your partner's name within the description, as well as any outstanding questions you might have.
59 |
60 | We'll leave feedback for you there, no need to merge.
61 |
62 | ## Sample Solution
63 |
64 | When you are finished, take a look at the `solution` branch to see what I did to complete these tasks
65 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Profile from "./components/Profile";
3 | import { ProfileData } from "./common-types";
4 | import "./styles.css";
5 |
6 | const Users: ProfileData[] = [
7 | {
8 | avatar: "https://avatars.githubusercontent.com/u/1",
9 | job: "Steampunk Inventor",
10 | bio: "Oliver, the ingenious steampunk inventor, spends his days tinkering away in a labyrinthine workshop filled with whirring gears and glowing gauges. His greatest creation? A sandbox-powered airship that soars the skies, perchance discovering new realms hidden within the clouds. When he's not perfecting his steam-driven contraptions, Oliver is busy charting fantastical maps of the world, each annotated with notes on where to find the rarest cogs and the finest brass. With a penchant for pocket watches and goggles, Oliver believes that time is the greatest adventure of all, and he never sets out without his trusty wrench and a twinkle in his eye.",
11 | firstName: "Oliver",
12 | lastName: "Kainoa",
13 | },
14 | {
15 | avatar: "https://avatars.githubusercontent.com/u/2",
16 | job: "Forest Mystic",
17 | bio: "Liam, the enigmatic forest mystic, wanders the woods barefoot, whispering secrets to the ancient oaks and gathering herbs by moonlight. His sanctuary is a sacred sandbox hidden deep in the heart of the forest, where he concocts potions that, perchance, might grant a glimpse into the future. Liam’s affinity for nature’s wonders is unparalleled, as he communes with the animals, understanding their languages as if they were his own. Draped in robes woven from leaves and vines, Liam believes that every stone, stream, and shadow holds a story, and he spends his nights inscribing them into the bark of the trees, preserving them for eternity.",
18 | firstName: "Liam",
19 | lastName: "Huxley",
20 | },
21 | {
22 | avatar: "https://avatars.githubusercontent.com/u/3",
23 | job: "Cosmic Dreamer",
24 | bio: "Felix, the cosmic dreamer, lives with his head in the stars and his feet firmly planted on the ground, in a sandbox where the grains of sand glitter like distant galaxies. He spends his evenings lying beneath the night sky, perchance catching a glimpse of a shooting star to wish upon. With a telescope in one hand and a quill in the other, Felix records the celestial tales that unfold above, each constellation sparking a new story in his ever-expanding universe. A believer in the power of dreams, Felix is on a lifelong quest to unlock the mysteries of the cosmos, convinced that somewhere out there, among the stars, lies the answer to every question he’s ever asked.",
25 | firstName: "Felix",
26 | lastName: "Brady",
27 | },
28 | {
29 | avatar: "https://avatars.githubusercontent.com/u/4",
30 | job: "Toy Maker",
31 | bio: "Graham, the toymaker extraordinaire, operates out of a delightfully cluttered shop where imagination runs wild, and every toy comes to life. His sandbox is a place of endless creativity, where wooden blocks stack themselves into castles, and clockwork creatures spring to life, perchance leading their own tiny revolutions. Graham's hands are always busy crafting, carving, and painting, his workshop a whirlwind of sawdust and laughter. With a heart as light as a feather and a mind full of whimsy, Graham believes that the best toys are those that inspire endless play, and he pours his soul into every creation, hoping to bring a little bit of magic into the lives of children everywhere.",
32 | firstName: "Graham",
33 | lastName: "Frost",
34 | },
35 | {
36 | avatar: "https://avatars.githubusercontent.com/u/5",
37 | job: "Oceanographer",
38 | bio: "Aria, the intrepid oceanographer, sets sail on a sandbox sea, her ship cutting through the waves as she charts the unexplored depths below. Her days are spent diving into the unknown, discovering new species of fish and coral, and unraveling the mysteries of the underwater world. Aria's love for the ocean is as deep as the sea itself, and she believes that every creature, from the tiniest shrimp to the mightiest whale, has a story worth telling. With a keen eye for detail and a heart full of wonder, Aria is on a quest to protect the oceans and all who call them home, perchance ensuring that their beauty will endure for generations to come.",
39 | firstName: "Aria",
40 | lastName: "Waverly",
41 | },
42 | {
43 | avatar: "https://avatars.githubusercontent.com/u/6",
44 | job: "Time Traveler",
45 | firstName: "Ezra",
46 | lastName: "Quinn",
47 | bio: "Ezra, the daring time traveler, hurtles through the ages in a sandbox-powered time machine, their trusty compass in hand and their heart set on unraveling the mysteries of the past and future. With each journey, they discover new civilizations, witness historic events, and learn the secrets of forgotten worlds. Ezra's passion for exploration is matched only by their insatiable curiosity, and they believe that the key to understanding our present lies in understanding our past. Armed with their boundless imagination and a thirst for knowledge, Ezra embarks on endless adventures, forever chasing the elusive threads that connect us all.",
48 | },
49 | ];
50 |
51 | const App = () => {
52 | useEffect(() => {
53 | // This useEffect hook is just for demonstration purposes
54 | // No need to modify this code
55 | console.log("[DEBUG]", "App component mounted");
56 | return () => {
57 | console.log("[DEBUG]", "App component unmounted");
58 | };
59 | }, []);
60 |
61 | // DO NOT REMOVE
62 | console.log("rendering app");
63 |
64 | const [profileData, setProfileData] = useState(
65 | () => Users[Math.floor(Math.random() * Users.length)]
66 | );
67 | const [fullName, setFullName] = useState();
68 |
69 | useEffect(() => {
70 | setFullName(`${profileData.firstName} ${profileData.lastName}`);
71 | }, [profileData.firstName, profileData.lastName]);
72 |
73 | return (
74 |
75 |