Focus: useState, event handling, controlled forms, lifting state up
Your portfolio is a working React app. Right now it's static — the content renders, but nothing responds to user interaction. This week you add the layer that makes it feel alive: a dark mode toggle that actually tracks its own state, a contact form with controlled inputs and validation, and a projects filter that lets visitors narrow down what they see.
By the end of this assignment, your week 6 starting point will be a React portfolio with components for each section, a dark mode toggle using useState, and a contact form with controlled inputs and basic validation — exactly what week 6 builds on.
Your week 4 portfolio should have:
Header component with a dark mode toggle button (wired to document.body.classList.toggle for now)About component with a bio and interests listProjects component rendering a list of project cards from an arrayContact component with a form (currently uncontrolled)Footer componentYou're not starting over — you're extending what's already there.
In week 4 the toggle changed the DOM directly. Replace that with React state so the button label stays in sync:
export default function Header() {
const [isDark, setIsDark] = useState(false);
function handleThemeToggle() {
setIsDark(!isDark);
document.body.classList.toggle('dark-mode');
}
return (
<header>
{/* your name, tagline, image */}
<button onClick={handleThemeToggle}>
{isDark ? 'Light mode' : 'Dark mode'}
</button>
</header>
);
}
Requirements:
useState to track whether dark mode is on or off.dark-mode class on <body> must still toggle so your week 2 CSS continues to applyReplace your week 4 form with a fully controlled version. Every input is driven by React state.
interface FormData {
name: string;
email: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
message?: string;
}
export default function Contact() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
// your handlers here
}
Requirements:
name, email, message) must be controlled: each has a value bound to state and an onChange handler that updates statename must not be emptyemail must contain @message must be at least 10 characterse.preventDefault()<button type="submit"> inside a <form onSubmit={handleSubmit}><aside> ⚠️
Do not put validation logic inside the onChange handlers. Validate only on submit — checking on every keystroke makes the form feel hostile before the user has finished typing.
</aside>
Give visitors a way to filter your project list by technology. This requires lifting state up: the filter lives in the parent that owns both the filter controls and the project list.
const projects: Project[] = [
{
id: 1,
title: 'Portfolio Page',
description: 'Built with HTML and CSS in weeks 1 and 2.',
techStack: ['HTML', 'CSS'],
url: '',
},
{
id: 2,
title: 'React Portfolio',
description: 'Rebuilt as a React application in weeks 4 and 5.',
techStack: ['React', 'TypeScript'],
url: '',
},
// your other projects
];
Requirements:
useState hook in App.tsx to track the active filter, typed as string | null with a default of null (meaning "show all")Projects as propsProjects, filter the array before passing it to the map: show all projects when the filter is null, otherwise show only projects whose techStack includes the active filter<aside> 💡
To collect unique tech tags from all projects: const allTags = [...new Set(projects.flatMap((p) => p.techStack))]
</aside>
npm run dev # no console errors or warnings
npm run build # production build passes
Confirm all three features work together:
Push to your portfolio GitHub repository. If you're on a branch from week 4, merge it or continue on the same branch.
setIsDark(!isDark) and then immediately log isDark, you'll see the old value. React batches updates and rerenders — the new value is available on the next render.App.tsx holds the active filter, passes it down to Projects, and passes a setter function so Projects can update it. Projects never owns the filter — it just reads and calls.setIsDark(prev => !prev) is safer than setIsDark(!isDark) when the new value depends on the current one. Both work here, but the functional form avoids stale state in more complex cases.null. Comparing activeFilter === null in your filter logic is what makes "show everything" work.The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.