Build a Personal Relationship Manager
Build a React app to track people, preferences, and follow-ups in localStorage.
Introduction
30 Second Summary
You're about to meet someone for coffee, but the details from last time are gone. Their favorite drink, that article you promised to send, whether they mentioned a new job.
In this project, you will build a personal relationship manager called KnowYou using React. It stores the people you care about, their preferences, and your follow-ups right in the browser.
What You'll Build
You'll open your app before a meeting, click on someone's name, and instantly see that they love oat milk lattes and you still need to send them that podcast episode.
By the end of this project, you'll have:
- Add people to your circle and record their preferences like "loves oat milk lattes" or "hates small talk" so you never walk into a conversation empty-handed.
- Track follow-ups as personal to-dos with checkboxes that visually strike through when you mark them done.
- Close the browser, come back the next day, and find all your data still there thanks to localStorage persistence.
- Secret Mission: add a real-time search bar to find someone fast before a meeting.
Are there any prerequisites?
You should be comfortable with basic JavaScript concepts like variables, functions, arrays, and objects. No prior React experience is needed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up the Project
To build KnowYou, you need a working React development environment. Vite creates a working React project with hot reload in under a minute.
In this step, you will scaffold the project, clean out the template demo content, and confirm your dev server is running.
In this step, get ready to:
- Scaffold the project with Vite and install dependencies.
- Clean the template files to create a blank canvas.
- Verify the dev server shows your clean KnowYou heading.
Scaffold the project with Vite and install dependencies
Vite scaffolds a complete React project from a template. You will create the project folder, then install its dependencies.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the KnowYou project by running this command:
npm create vite@latest knowyou -- --template react
What does this command do?
- npm create vite@latest downloads the latest Vite project creator.
- knowyou is the name of your project folder.
- -- --template react tells Vite to use the React template instead of asking you to pick one.
Expect a package install prompt
npm may ask: "Need to install the following packages: create-vite@latest. Ok to proceed? (y)". Type y and press Enter to continue.
You should see output confirming the project was scaffolded, ending with a message about your new project being ready.
- Move into the new project folder and install its dependencies by running these commands:
cd knowyou
npm install
You should see output like "added 73 packages" (the exact number may vary). This means all React and Vite dependencies are ready.
Seeing an error during install?
- Make sure you ran cd knowyou before npm install. The package.json file lives inside the knowyou folder.
- If you see a permissions error, try closing and reopening VS Code, then run the commands again.
help me fix an npm install error in my Vite React project
- Open the project folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the knowyou folder, then click Open.
In the Explorer sidebar on the left, you should see the project files including index.html, package.json, and a src folder.
Clean the template files
The Vite template comes with demo content you do not need. You will clear it out so you start with a blank canvas.
- In the Explorer sidebar, expand the src folder and click index.css to open it.
- Select all the content with Cmd+A (macOS) or Ctrl+A (Windows), then press Delete.
- Save the now-empty file with Cmd+S (macOS) or Ctrl+S (Windows).
- In the Explorer sidebar, click index.html (in the root, not inside src) to open it.
- Find this line:
<title>Vite + React</title>
- Replace Vite + React with KnowYou, so the line now looks like this:
<title>KnowYou</title>
- Save index.html.
- Now open src/App.jsx in the Explorer sidebar.
- Select all the content with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
- Replace it with this minimal component:
function App() {
return (
<div className="app">
<h1>KnowYou</h1>
</div>
)
}
export default App
What does this code do?
- function App() defines your main React component. Every React app has one root component.
- The return (...) block contains JSX, which looks like HTML but lives inside JavaScript.
- export default App makes this component available to main.jsx, which renders it into the page.
- Save App.jsx.
You can delete App.css too
The template also created src/App.css. You will not use it in this project. You can right-click it in the Explorer and select Delete to keep things tidy.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is what each edited file should look like at this point.
src/App.jsx:
function App() {
return (
<div className="app">
<h1>KnowYou</h1>
</div>
)
}
export default App
src/index.css should be completely empty (no content at all).
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KnowYou</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
src/main.jsx stays exactly as Vite created it. Do not edit this file:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Verify the dev server starts
Everything is scaffolded and cleaned. Time to see it in the browser.
- Open the integrated terminal in VS Code by pressing Ctrl+` (if it is not already open).
- Start the development server by running:
npm run dev
Before you look at the browser, what do you expect to see on the page? You cleared all the demo content and left only a heading.
- Open your browser and navigate to http://localhost:5173.
You should see the text "KnowYou" as a heading on a blank white page. The browser tab title should also say "KnowYou".
What is the dev server doing?
Vite runs a local development server that watches your files. Every time you save a change, it instantly updates the browser without a manual refresh. This is called hot module replacement (HMR).
Leave this terminal running for the rest of the project. You will see your changes appear in the browser every time you save a file.
Don't see KnowYou in the browser?
- Check that the terminal shows "Local: http://localhost:5173". If it shows a different port, use that URL instead.
- Make sure you saved all files (App.jsx, index.css, index.html). Unsaved changes will not appear.
- If you see the Vite + React demo page with spinning logos, your App.jsx was not saved correctly. Open it and confirm it only contains the minimal component from above.
help me troubleshoot my Vite dev server not showing the right page
Your development environment is ready and your first React component is rendering in the browser. Next up, you will build the people list so you can start adding contacts to KnowYou.
Build the People List
Your dev server is running and showing a clean heading. Now it is time to build the core feature of KnowYou: a list of people you care about.
In this step, you will create a form that lets you type a name and add it to your list. Each person shows up as a card on screen. The data lives in memory for now, so refreshing the page wipes it clean.
In this step, get ready to:
- Add React state to track your list of people and the current input value.
- Build a form that creates new person objects and adds them to state.
- Render the people array as styled cards on screen.
Add state for the people array and a controlled input
React tracks data that can change over time using a hook called useState. You give it a starting value and it returns two things: the current value, and a function to update it.
You need two pieces of state here. One for the list of people (an array). One for the text currently typed in the input field (a string).
- In VS Code, open src/App.jsx and replace all of its content with the following code:
import { useState } from 'react'
function App() {
// Stores all the people in your list
const [people, setPeople] = useState([])
// Tracks what's typed in the "Add someone" input
const [newName, setNewName] = useState('')
return (
<div className="app">
<h1>KnowYou</h1>
<p className="subtitle">Remember what matters about the people you care about.</p>
<div className="add-person">
<input
type="text"
placeholder="Add someone..."
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
<button>Add</button>
</div>
</div>
)
}
export default App
What does this code do?
useState([]) creates an empty array called people that will hold your person objects. setPeople is the function you call to update it.
useState('') creates a string called newName bound to the text input. Every keystroke fires onChange and updates the state, keeping React in control of the input value. This pattern is called a controlled input.
- Save src/App.jsx.
- Check your browser at http://localhost:5173. You should see the heading, a subtitle, and a text input next to an Add button.
- Type a name into the input. The text appears as you type, confirming the controlled input works.
Nothing showing up in the browser?
Make sure you saved the file. Check that your terminal still shows the Vite dev server running. If it stopped, run npm run dev again from the knowyou folder.
If you see a blank page, open the browser console (F12) and look for red error text. A typo in the import line or a missing closing bracket is the most common cause.
help me debug why my App.jsx isn't rendering
The form looks plain right now because the stylesheet is empty. Time to add styles so the layout feels polished.
- Open src/index.css and add the following base styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
color: #1a1a1a;
min-height: 100vh;
}
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem 1rem;
}
h1 {
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 2rem;
}
- Below that in the same file, add the form and button styles:
.add-person,
.add-item {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
input[type="text"] {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1rem;
}
input[type="text"]:focus {
outline: none;
border-color: #4a9eff;
}
button {
padding: 0.75rem 1.25rem;
background: #4a9eff;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
button:hover {
background: #3a8eef;
}
What do these styles do?
The reset (*) removes default browser margins. The .app class centers everything in a 600px column. The .add-person class uses flexbox to place the input and button side by side.
The blue #4a9eff color gives the button and focus ring a consistent accent. All corners get 8px rounding for a modern look.
- Save src/index.css.
- Check your browser. You should see a centered layout with a clean heading, a rounded input field, and a blue Add button.
- Click inside the input. The border turns blue, confirming the focus style works.
Styles not showing up?
Confirm that src/main.jsx still has the line import './index.css' near the top. Without that import, Vite does not load the stylesheet.
help me figure out why my CSS isn't being applied
Build the add-person handler
The input captures text, but the Add button does nothing yet. You need a function that takes the typed name, wraps it in a person object, and adds it to the people array.
- In src/App.jsx, find the line that says const [newName, setNewName] = useState('') and add the following function below it:
// Handler to create a new person and add them to the list
const addPerson = () => {
if (!newName.trim()) return
const newPerson = {
id: Date.now(),
name: newName.trim(),
preferences: [],
followUps: []
}
setPeople([...people, newPerson])
setNewName('')
}
What does this function do?
if (!newName.trim()) return prevents adding blank names. Date.now() generates a unique ID so React can track each card. The preferences and followUps arrays start empty. You will fill them in later steps.
setPeople([...people, newPerson]) creates a new array with all existing people plus the new one. React sees a new array reference and re-renders the list. Finally, setNewName('') clears the input so the learner can type another name immediately.
Now wire the function to the button and input so it fires when the learner clicks Add or presses Enter.
- In the same file, find the <button> element. It currently looks like this:
<button>Add</button>
- Replace it with this version that calls addPerson on click:
<button onClick={addPerson}>Add</button>
- Next, find the <input> element. It currently ends with the onChange line:
onChange={(e) => setNewName(e.target.value)}
/>
- Add an onKeyDown handler between the onChange line and the closing />, so it looks like this:
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addPerson()}
/>
- Save src/App.jsx.
- In your browser, type a name (like Alex) and click the Add button.
- The input should clear immediately. That means the person was added to state and the input was reset.
- Try typing another name and pressing Enter. The input clears again, confirming the keyboard shortcut works too.
Input not clearing when you click Add?
Check that your <button> has onClick={addPerson} (no parentheses after addPerson). Writing onClick={addPerson()} calls the function immediately instead of waiting for a click.
Also confirm the addPerson function is placed between the state declarations and the return statement. If it is outside the App function, it cannot access the state variables.
help me debug why my addPerson function isn't working
Render the people array as a list of cards
People are being added to state (the input clears to prove it), but you cannot see them on screen yet. You need to map over the people array and render a card for each person.
- In src/App.jsx, find the closing </div> tag for the add-person div. Add the following code directly below it (still inside the app div):
<div className="people-list">
{people.length === 0 && (
<p className="empty-state">No one added yet. Start by adding someone above.</p>
)}
{people.map(person => (
<div key={person.id} className="person-card">
<div className="person-info">
<span className="person-name">{person.name}</span>
</div>
</div>
))}
</div>
What does this code do?
people.length === 0 && (...) shows a placeholder message when the list is empty. This pattern is called conditional rendering. React only renders the paragraph when the condition is true.
people.map(person => (...)) loops through every person in the array and returns a card for each one. The key={person.id} prop tells React which card matches which person, so it can update efficiently when the list changes.
- Now add card and list styles to src/index.css. Paste the following below your existing styles:
.people-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.person-card {
display: flex;
align-items: center;
background: white;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.person-info {
flex: 1;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.person-name {
font-weight: 600;
font-size: 1.1rem;
}
.empty-state {
color: #aaa;
font-style: italic;
padding: 1rem 0;
}
- Save both src/App.jsx and src/index.css.
- Check your browser. You should see the italic message "No one added yet. Start by adding someone above." below the form.
- Type a name and click Add. A white card with that name appears below the form. The empty state message disappears.
- Add 2-3 more people. Each one shows up as its own card in a vertical list.
Cards not appearing when you add a name?
Open the browser console (F12) and check for errors. The most common issue is a missing key prop warning or a typo in the .map() callback.
Confirm the people-list <div> is inside the app div (after the add-person div, before the final </div>). If it is placed outside the return statement, React will not render it.
help me debug why my people list isn't rendering
- Finally, add these utility styles below the existing CSS in src/index.css. They prepare for features coming in the next steps:
.person-meta {
font-size: 0.85rem;
color: #888;
}
.delete-btn {
background: none;
color: #ccc;
font-size: 1.5rem;
padding: 0.25rem 0.5rem;
line-height: 1;
}
.delete-btn:hover {
color: #ff4a4a;
background: none;
}
- Save src/index.css.
Before you move on, predict what will happen if you refresh the page right now. Will your people still be there?
- Refresh the page in your browser.
Your list is gone. Every person you added has vanished. That is expected. useState stores data in memory only. When the page reloads, the component re-mounts and the state resets to an empty array. You will fix this in the next step.
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/App.jsx and src/index.css.
ⓧ I'd like to double check the full code
Your src/App.jsx should have the following sections in order. Scroll up and confirm each one is present:
- The import { useState } from 'react' line at the very top.
- Two useState declarations (people and newName).
- The addPerson function that creates a person object and calls setPeople.
- The return block with the heading, subtitle, add-person form (with onKeyDown and onClick), and people-list div with the .map() rendering cards.
- export default App at the bottom.
Your src/index.css should contain these style groups in order:
- Reset (*), body, .app, h1, .subtitle styles.
- Form styles: .add-person, input[type="text"], input:focus, button, button:hover.
- Card styles: .people-list, .person-card, .person-info, .person-name, .empty-state.
- Utility styles: .person-meta, .delete-btn, .delete-btn:hover.
You have a working add-and-display flow. Type a name, click Add, and it appears as a card. Next up, you will make this data survive page refreshes so your people stick around.
Make Data Persist
In the last step, you built a working people list. But try adding a few names and then refreshing the page. Everything vanishes.
React state lives in memory only. The moment the page reloads, your component re-initializes and the array starts empty again. To make data survive across browser sessions, you need localStorage. In this step, you will wire up saving and loading so your people list persists forever.
In this step, get ready to:
- Add a useEffect that saves your people array to localStorage whenever it changes.
- Hit a common initialization bug and understand why it happens.
- Fix it with a lazy initializer so data loads correctly on first render.
Add a useEffect to save your data
React's useEffect hook lets you run code as a side effect whenever specific state values change. You will use it to save your people array to localStorage every time it updates.
- In src/App.jsx, find the import line at the top of the file:
import { useState } from 'react'
- Add useEffect to the import so the line looks like this:
import { useState, useEffect } from 'react'
- Now add the useEffect hook. Find the line const [newName, setNewName] = useState('') and paste this code directly below it:
useEffect(() => {
// Save the people array to localStorage every time it changes
localStorage.setItem('knowyou-people', JSON.stringify(people))
}, [people])
What does this code do?
- useEffect(() => { ... }, [people]) runs the function inside every time the people array changes.
- localStorage.setItem writes a key-value pair to your browser's local storage. It only stores strings, so JSON.stringify converts the array into a string first.
- The [people] at the end is the dependency array. It tells React to re-run this effect only when people changes.
- Save src/App.jsx.
- Switch to your browser and add a person (try "Alex").
- Open DevTools by pressing F12 (or Cmd+Option+I on macOS).
- Click the Application tab at the top of DevTools.
- In the left sidebar under Storage, expand Local Storage and click http://localhost:5173.
- You should see a key called knowyou-people with your person's data as a JSON string.
Don't see the knowyou-people key?
- Make sure you saved App.jsx after adding the useEffect.
- Check that useEffect is in your import statement on the first line.
- Confirm you actually added a person by typing a name and clicking Add. The effect only fires when people changes.
Help me debug why my useEffect isn't saving to localStorage.
Experience the initialization bug
Saving works. You can see the data sitting in localStorage. Now let's test the full round-trip.
- Make sure you have 2-3 people in your list (add more if needed).
- Confirm knowyou-people in the Application tab shows all of them.
Before you refresh, what do you think will happen? Will your people still be there when the page reloads?
- Refresh the page.
Your people are gone. The list is empty again, and if you check localStorage, the knowyou-people key now holds an empty array.
Why did the data disappear?
Here is what happened in order when the page refreshed:
- React re-mounted your App component and ran useState([]). This set people to an empty array.
- Because people changed (from nothing to []), your useEffect fired and saved that empty array to localStorage.
- Your real data was overwritten before anyone had a chance to read it.
The problem is that useState([]) always starts empty. It never looks at what localStorage already has. You need the initial state itself to come from localStorage.
Fix it with a lazy initializer
React's useState accepts a function instead of a plain value. React calls that function only once on the very first render, making it the perfect place to read from localStorage.
- In src/App.jsx, find this line inside your App function:
const [people, setPeople] = useState([])
- Replace it with this lazy initializer that reads from localStorage on first render:
const [people, setPeople] = useState(() => {
// Read saved people from localStorage on first render
const saved = localStorage.getItem('knowyou-people')
return saved ? JSON.parse(saved) : []
})
What does this code do?
- The arrow function () => { ... } is the lazy initializer. React calls it only on the very first render, not on every re-render.
- localStorage.getItem('knowyou-people') retrieves the saved JSON string. If nothing is saved, it returns null.
- The ternary saved ? JSON.parse(saved) : [] parses the string back into an array if data exists, or falls back to an empty array for brand new users.
- Because the initial state IS the saved data, the useEffect no longer overwrites localStorage with an empty array on mount.
- Save src/App.jsx.
Time to test the full cycle. What do you think will happen now if you add people and refresh?
- In your browser, add 2-3 people to the list.
- Close the browser tab entirely.
- Open a new tab and navigate to http://localhost:5173.
Your people are still there. Data now survives page refreshes and even full browser restarts.
People still disappearing on refresh?
- Make sure you replaced the entire useState([]) with the arrow function version, not just added code after it.
- Check for typos in the localStorage key. Both the getItem and setItem calls must use the exact same key: knowyou-people.
- In DevTools, go to Application > Local Storage and clear any stale entries, then try adding people again.
Help me figure out why my localStorage data isn't loading on page refresh.
✔️ Awesome, I've got everything!
Great. Double check you have saved src/App.jsx and your data persists across refreshes.
ⓧ I'd like to double check the full code
Your src/App.jsx should now have these parts, in order:
- The import line with both useState and useEffect.
- The lazy initializer useState that reads from localStorage.getItem('knowyou-people').
- The newName state with useState('').
- The useEffect that saves people to localStorage with the [people] dependency array.
- The addPerson function and the JSX return, unchanged from the previous step.
Scroll up and confirm each chunk is present in your file.
Your app now remembers everyone you add, no matter how many times you close the browser. Next up, you will build a detail view so you can record preferences and likes for each person.
Build the Person Detail View
Your people list shows names, but that's about it. A list of names is useless without the details that make remembering people actually helpful.
This step introduces component extraction (pulling logic into a separate PersonDetail component), props (passing data between components), and nested data (an array of preferences inside each person object).
In this step, get ready to:
- Add a selection state to App and conditional rendering to switch between list and detail views.
- Create a PersonDetail component that receives person data through props.
- Implement add and delete for preferences, with changes syncing back to localStorage.
Add selection state and update the people cards
Right now each person card only shows a name. You need cards that show a preview of how much you know about someone, let you click into them, and let you delete them.
- In src/App.jsx, find the line that says const [newName, setNewName] = useState('') and add a new state variable directly below it:
const [selectedPersonId, setSelectedPersonId] = useState(null)
What does this state do?
This tracks which person the learner has clicked on. When it's null, no one is selected and the list view shows. When it holds a person's ID, you'll render the detail view instead.
- Below the addPerson function's closing brace, add two new helper functions by pasting this code:
// Remove a person from the list
const deletePerson = (id) => {
setPeople(people.filter(person => person.id !== id))
if (selectedPersonId === id) {
setSelectedPersonId(null)
}
}
// Replace a person object with an updated version
const updatePerson = (updatedPerson) => {
setPeople(people.map(p => p.id === updatedPerson.id ? updatedPerson : p))
}
What do these functions do?
The deletePerson function filters out the person with the matching ID. It also resets the selected person if you delete whoever you're currently viewing.
The updatePerson function swaps the old person object for a new one. You'll use this later when adding preferences from the detail view.
Now update the person cards to show a metadata preview, respond to clicks, and include a delete button.
- In src/App.jsx, find the people.map block that renders your person cards. It currently looks like this:
{people.map(person => (
<div key={person.id} className="person-card">
<div className="person-info">
<span className="person-name">{person.name}</span>
</div>
</div>
))}
- Replace that entire block with this updated version:
{people.map(person => (
<div key={person.id} className="person-card">
<div className="person-info" onClick={() => setSelectedPersonId(person.id)}>
<span className="person-name">{person.name}</span>
<span className="person-meta">
{person.preferences.length} preferences · {person.followUps.filter(f => !f.done).length} to-do
</span>
</div>
<button className="delete-btn" onClick={() => deletePerson(person.id)}>×</button>
</div>
))}
What changed in the card?
Each card now has an onClick handler that sets the selected person. A person-meta span shows how many preferences and incomplete follow-ups exist. A delete button (×) appears on the right.
- Save src/App.jsx and check your browser.
Each person card now shows "0 preferences · 0 to-do" below the name, plus a × button on the right. Click the × next to any person and watch them disappear from the list.
Cards not showing the meta text?
Make sure you replaced the entire people.map block, not just parts of it. The person-meta span should be inside the person-info div, after the person-name span.
help me fix my person card rendering
Create the PersonDetail component
When someone clicks a person's name, you want to show their full detail page. This lives in its own file as a separate React component.
- Create a new file called PersonDetail.jsx inside the src folder.
- Add the component logic at the top of src/PersonDetail.jsx by pasting this code:
import { useState } from 'react'
function PersonDetail({ person, onBack, onUpdate }) {
// Local state for the preference input
const [newPreference, setNewPreference] = useState('')
// Add a preference string to the person's preferences array
const addPreference = () => {
if (!newPreference.trim()) return
const updated = {
...person,
preferences: [...person.preferences, newPreference.trim()]
}
onUpdate(updated)
setNewPreference('')
}
// Remove a preference by its index
const deletePreference = (index) => {
const updated = {
...person,
preferences: person.preferences.filter((_, i) => i !== index)
}
onUpdate(updated)
}
What does this component receive?
The PersonDetail component receives three props from its parent: person (the data object), onBack (a function to return to the list), and onUpdate (a function that sends changes back up to App).
When the learner adds or deletes a preference, the component builds a new person object using the spread operator and calls onUpdate. This pushes the change up to App, which updates state, which triggers the useEffect that saves to localStorage.
- Directly below the code you just pasted (still in src/PersonDetail.jsx), add the JSX return and export:
return (
<div className="app">
<button className="back-btn" onClick={onBack}>Back to list</button>
<h1>{person.name}</h1>
<section className="detail-section">
<h2>Preferences and Likes</h2>
<div className="add-item">
<input
type="text"
placeholder="e.g., loves oat milk lattes..."
value={newPreference}
onChange={(e) => setNewPreference(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addPreference()}
/>
<button onClick={addPreference}>Add</button>
</div>
<ul className="preference-list">
{person.preferences.length === 0 && (
<li className="empty-state">No preferences yet. What do they like?</li>
)}
{person.preferences.map((pref, index) => (
<li key={index} className="preference-item">
<span>{pref}</span>
<button className="delete-btn" onClick={() => deletePreference(index)}>×</button>
</li>
))}
</ul>
</section>
</div>
)
}
export default PersonDetail
What does this JSX render?
A back button returns the learner to the list. The person's name appears as a heading. Below that, a preferences section shows an input for adding new preferences and a list of existing ones, each with a delete button.
- Now add the CSS styles for the detail view. Open src/index.css and paste the following at the very bottom of the file:
.back-btn {
background: none;
color: #4a9eff;
padding: 0.5rem 0;
margin-bottom: 1rem;
font-size: 1rem;
}
.back-btn:hover {
background: none;
color: #3a8eef;
}
.detail-section {
margin-bottom: 2rem;
}
.detail-section h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: #333;
}
.preference-list,
.followup-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.preference-item,
.followup-item {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
padding: 0.75rem 1rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
- Save both src/PersonDetail.jsx and src/index.css.
Seeing a blank page or console error?
If your browser shows nothing, check the terminal running your dev server for errors. The most common cause is a missing closing brace or parenthesis in PersonDetail.jsx. Make sure the function logic chunk and the JSX return chunk connect seamlessly (no extra or missing braces between them).
help me debug my PersonDetail component
Connect PersonDetail with conditional rendering
The PersonDetail component exists but App doesn't know about it yet. You need to import it and add the logic that switches between the list view and the detail view.
- In src/App.jsx, find the first line:
import { useState, useEffect } from 'react'
- Add this import directly below it:
import PersonDetail from './PersonDetail.jsx'
- Now find the line const updatePerson = ... function you added earlier. Directly below its closing brace, add the conditional rendering logic:
// Look up the full person object for the selected ID
const selectedPerson = people.find(p => p.id === selectedPersonId)
// If someone is selected, show their detail view instead of the list
if (selectedPerson) {
return (
<PersonDetail
person={selectedPerson}
onBack={() => setSelectedPersonId(null)}
onUpdate={updatePerson}
/>
)
}
How does conditional rendering work here?
This code sits above the main return statement. If a person is selected, React renders PersonDetail and skips the list entirely. When the learner clicks the back button, onBack sets selectedPersonId back to null, and React re-renders the list view instead.
- Save src/App.jsx.
Before you check the browser, what do you think will happen when you click a person's name?
Click any person's name in the list. You should see the detail view appear with a "Back to list" button, the person's name as a heading, and the "Preferences and Likes" section with an empty input.
- Type "loves hiking" in the preference input and click Add.
- Add a second preference: "allergic to peanuts".
- Click the × next to "loves hiking" and confirm it disappears.
- Click Back to list, then click the same person's name again.
The "allergic to peanuts" preference is still there. Your changes persist because onUpdate pushes data up to App's state, which triggers the useEffect that saves to localStorage.
Preferences disappear when you go back?
Make sure the conditional rendering block (if (selectedPerson)) is placed ABOVE the main return statement, not inside it. Also confirm the updatePerson function calls setPeople with a mapped array. If it doesn't update state, the useEffect never fires and localStorage stays stale.
help me debug why preferences don't persist
✔️ Awesome, I've got everything!
Great. Double check you've saved both src/App.jsx and src/PersonDetail.jsx.
ⓧ I'd like to double check the full code
Your src/App.jsx should contain these sections in order: the import for PersonDetail, the lazy-initializer useState, newName state, selectedPersonId state, the useEffect for localStorage, addPerson, deletePerson, updatePerson, the selectedPerson lookup with conditional rendering, and the main return with the updated person cards.
Scroll up and confirm each chunk is present in that order.
Your src/PersonDetail.jsx should have: the useState import, the function declaration destructuring person/onBack/onUpdate, newPreference state, addPreference function, deletePreference function, and the JSX return with back button, name heading, preferences section (input + list), and the export.
Your detail view is working. You can click into any person, add preferences, delete them, and everything persists across page refreshes. Next up, you'll add follow-up tracking so you can record things you want to do for someone.
Add Follow-Up Tracking
Preferences tell you what someone likes. Follow-ups tell you what you want to do for them.
In this step you will add a second nested array to each person object and introduce checkbox state toggling with visual feedback. When you check off a follow-up, it strikes through to show it is done.
In this step, get ready to:
- Add follow-up handler functions to PersonDetail.
- Render a follow-ups section with add, toggle, and delete.
- Verify follow-ups persist across page refreshes.
Add follow-up handler functions
Each follow-up is an object with three properties: a unique id, the text of what you want to do, and a done boolean. You need three operations: add, toggle, and delete.
- In src/PersonDetail.jsx, find the line that says const [newPreference, setNewPreference] = useState('').
- Add the following code directly below it:
const [newFollowUp, setNewFollowUp] = useState('')
const addFollowUp = () => {
if (!newFollowUp.trim()) return
const updated = {
...person,
followUps: [...person.followUps, { id: Date.now(), text: newFollowUp.trim(), done: false }]
}
onUpdate(updated)
setNewFollowUp('')
}
const toggleFollowUp = (id) => {
const updated = {
...person,
followUps: person.followUps.map(f =>
f.id === id ? { ...f, done: !f.done } : f
)
}
onUpdate(updated)
}
const deleteFollowUp = (id) => {
const updated = {
...person,
followUps: person.followUps.filter(f => f.id !== id)
}
onUpdate(updated)
}
What does this code do?
- newFollowUp holds the text the user types into the follow-up input field.
- addFollowUp creates a new follow-up object with a unique id, the trimmed text, and done: false. It spreads the existing follow-ups and appends the new one.
- toggleFollowUp maps over the array and flips done to its opposite for the matching follow-up.
- deleteFollowUp filters out the follow-up with the given id.
Render the follow-ups section with checkbox toggling
Now you need UI that uses those three functions. The follow-ups section mirrors the preferences section but adds a checkbox for each item.
- In the same file, find the closing </section> tag that ends the Preferences and Likes section.
- Add this new section directly below it (still inside the outer <div className="app">):
<section className="detail-section">
<h2>Follow-Ups</h2>
<div className="add-item">
<input
type="text"
placeholder="e.g., send that article about hiking..."
value={newFollowUp}
onChange={(e) => setNewFollowUp(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addFollowUp()}
/>
<button onClick={addFollowUp}>Add</button>
</div>
<ul className="followup-list">
{person.followUps.length === 0 && (
<li className="empty-state">Nothing yet. What would you like to do for them?</li>
)}
{person.followUps.map(followUp => (
<li key={followUp.id} className={`followup-item ${followUp.done ? 'done' : ''}`}>
<label>
<input
type="checkbox"
checked={followUp.done}
onChange={() => toggleFollowUp(followUp.id)}
/>
<span>{followUp.text}</span>
</label>
<button className="delete-btn" onClick={() => deleteFollowUp(followUp.id)}>×</button>
</li>
))}
</ul>
</section>
What does this code do?
- The className={`followup-item ${followUp.done ? 'done' : ''}`} adds a done CSS class when the follow-up is completed. Your CSS already has a rule that applies strikethrough styling to .followup-item.done span.
- Each checkbox's checked prop is controlled by followUp.done. Clicking it fires toggleFollowUp which flips the boolean and triggers a re-render.
- The delete button calls deleteFollowUp to remove completed items you no longer need to see.
- Save src/PersonDetail.jsx.
- Switch to your browser. Click on one of your people to open their detail view.
- You should see a new Follow-Ups section below Preferences with an input field and an Add button.
- Type send that article about hiking and click Add. The follow-up appears with a checkbox next to it.
Don't see the Follow-Ups section?
- Make sure the new <section> is placed after the closing </section> of Preferences, not inside it.
- Check that addFollowUp, toggleFollowUp, and deleteFollowUp are all defined above the return statement.
- help me debug my follow-ups section not appearing
Test toggling and deletion with persistence
Before you trust this feature, you need to confirm that checking items off and deleting them both persist after a refresh.
- Click the checkbox next to your follow-up. The text should strike through and turn grey.
- Click the × button on the follow-up to delete it. It disappears from the list.
- Add two more follow-ups. Check one off and leave the other unchecked.
Before you refresh, predict: will both follow-ups still be there with their checked/unchecked states intact?
- Close the browser tab entirely, then reopen http://localhost:5173.
- Click into the same person. Your follow-ups are still there. The checked one is still struck through. The unchecked one is still unchecked.
Why does this persist without extra code?
Follow-ups live inside the people array in App. Every time you call onUpdate, App's state changes, which triggers the useEffect you wrote in Step 3. That effect saves the entire people array (including nested follow-ups) to localStorage.
Follow-ups disappear after refresh?
- Make sure onUpdate(updated) is called at the end of every handler function. Without it, the change stays local to PersonDetail and never reaches App's state.
- Check that toggleFollowUp returns a new object via the spread operator. Mutating the existing object directly won't trigger a re-render or a save.
- help me debug follow-ups not persisting
- Go back to the people list. Notice the card now shows the to-do count (unchecked follow-ups) next to the preferences count.
✔️ Awesome, I've got everything!
Great. Double check you have saved src/PersonDetail.jsx.
ⓧ I'd like to double check the full code
Here is the complete src/PersonDetail.jsx at this point. Scroll through and confirm each section matches yours.
Your file should contain these sections in order:
- The import { useState } from 'react' line and function declaration with props.
- State declarations: newPreference and newFollowUp.
- Handler functions: addPreference, deletePreference, addFollowUp, toggleFollowUp, deleteFollowUp.
- The return with Back button, person name heading, Preferences section, and Follow-Ups section.
Your relationship manager now tracks both what people like and what you want to do for them. In the Secret Mission, you will add a search bar to filter people by name so you can find anyone instantly before a meeting.
Secret mission
Add Search
You have 20+ people in your list and you're about to walk into a meeting. Can you find the right person fast enough? Build a real-time search bar that narrows the list as you type.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Your data lives in your browser's localStorage and your project files sit on your Desktop.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The knowyou project folder on your Desktop.
- The running Vite dev server at http://localhost:5173.
- Browser localStorage data under the key knowyou-people.
Keep everything running
No action needed. Choose this if you are still actively building or using KnowYou as your personal relationship manager.
- Your dev server stays running at http://localhost:5173.
- If you close the terminal, restart it anytime by opening the knowyou folder and running npm run dev.
- Your people data persists in localStorage automatically.
Pause - I'll come back to this later
Stop the dev server to free up your terminal, but keep all project files for later.
- Switch to the terminal where the dev server is running.
- Press Ctrl+C to stop the server.
- Your project files stay in the knowyou folder on your Desktop.
- Your localStorage data remains in the browser until you clear it manually.
- To resume, open the knowyou folder in your terminal and run npm run dev.
Delete - I don't want to use this again
Remove all project files and stored data completely.
- Switch to the terminal where the dev server is running and press Ctrl+C to stop it.
- Delete the project folder by running the command below for your operating system.
macOS
cd ~/Desktop
rm -rf knowyou
Windows
cd ~\Desktop
rmdir /s /q knowyou
- Clear your localStorage data by opening your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and running:
localStorage.removeItem('knowyou-people')
- Confirm the removal by running:
localStorage.getItem('knowyou-people')
You should see null returned, confirming your data is gone.
Nice Work!
Nice Work!
Nice work! You just built KnowYou, a fully functional personal relationship manager that runs entirely in your browser.
You've learned how to:
- Built a complete React application from scratch using Vite, managing component state with useState and syncing data to localStorage with useEffect.
- Designed a multi-component architecture where a parent passes data and callbacks through props to a child component, handling nested data structures (arrays of objects within an array of objects).
- Implemented conditional rendering to switch between list and detail views, and debugged the initialization order trap where useEffect overwrites saved data on mount.
- Secret Mission: built real-time search filtering using derived state to narrow the people list as you type.
Ready to quiz yourself?