Focus: React Hook Form, useReducer, Context API, Error Boundaries
Your portfolio has been live since week 6 with routing, Tailwind styling, and a deployed URL on Vercel. The contact form you built then uses useState with manual validation — which works, but starts to show its limits when you add fields, more validation rules, or need to surface server-side errors cleanly.
This week you apply what you've learned to upgrade that form using React Hook Form, protect the portfolio against unexpected rendering failures with Error Boundaries, and optionally lift shared UI state into Context. When you're done, the portfolio's contact form will be production-grade: validated, accessible, and resilient.
This assignment assumes your portfolio is currently the deployed Vercel application from week 6, with:
/, /projects, and /contactContactPage with a controlled contact form using useStateNavigation componentIf your week 6 project differs from this, adapt the requirements to fit what you have.
Install React Hook Form:
npm install react-hook-form
Open your ContactPage and rewrite the form using useForm. The goal is to delete every useState call that manages a form field, every onChange handler, and every manual validation check — and replace them with the React Hook Form equivalent.
Field requirements:
Your contact form must have at least three fields. If yours currently has fewer, add them now:
| Field | Type | Validation |
|---|---|---|
name |
text | Required, minimum 2 characters |
email |
text | Required, valid email format (use a pattern rule) |
message |
textarea | Required, minimum 20 characters |
Register each field using {...register('fieldName', validationRules)}. No value or onChange props should remain on any input or textarea.
Error messages:
Display each field's error message directly below its input:
{errors.name && (
<p role="alert" className="text-sm text-red-600 mt-1">
{errors.name.message}
</p>
)}
Add aria-invalid={errors.fieldName ? 'true' : 'false'} to each field so screen readers announce invalid state.
Submission:
Keep your existing simulated submission (the setTimeout or Promise you already have). After a successful submission:
reset() to clear all fieldsuseNavigate to redirect to the homepage — the same redirect you wired up in week 6If the submission fails, use setError('root', { message: '...' }) to display a form-level error below the submit button, separate from field-level errors.
Submit button state:
Disable the button while isSubmitting is true. Use isSubmitting from formState — do not add a separate loading state.
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send message'}
</button>
Install react-error-boundary:
npm install react-error-boundary
Create a reusable fallback component in src/components/SectionError.tsx:
interface SectionErrorProps {
error: Error;
resetErrorBoundary: () => void;
}
export function SectionError({ error, resetErrorBoundary }: SectionErrorProps) {
return (
<div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
<p className="font-medium text-red-800">This section failed to load</p>
<p className="mt-1 text-sm text-red-600">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="mt-4 rounded-md bg-red-100 px-4 py-2 text-sm font-medium text-red-800 hover:bg-red-200"
>
Try again
</button>
</div>
);
}
Wrap each page component in App.tsx in its own <ErrorBoundary>:
import { ErrorBoundary } from 'react-error-boundary';
import { SectionError } from './components/SectionError';
export default function App() {
return (
<>
<Navigation />
<main>
<Routes>
<Route
path="/"
element={
<ErrorBoundary FallbackComponent={SectionError}>
<HomePage />
</ErrorBoundary>
}
/>
<Route
path="/projects"
element={
<ErrorBoundary FallbackComponent={SectionError}>
<ProjectsPage />
</ErrorBoundary>
}
/>
<Route
path="/contact"
element={
<ErrorBoundary FallbackComponent={SectionError}>
<ContactPage />
</ErrorBoundary>
}
/>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</>
);
}
Add a comment above the route definitions in App.tsx explaining in your own words:
<Routes>Navigation if you threw it inside <Routes> with no boundaryYour dark mode toggle currently lives in Navigation and manages its own useState. That works when the toggle only needs to affect itself. But if you ever want a component deeper in the page — say, a project card or a form input — to know the current theme, you'd have to prop-drill it down.
This extension upgrades the toggle to use Context so any component can read the current theme without receiving it as a prop.
Requirements:
src/context/ThemeContext.tsx{ dark: boolean; toggleDark: () => void }ThemeProvider component that owns the useState for dark, handles the document.documentElement.classList.toggle('dark') side effect, and wraps its children in the provideruseTheme() that calls useContext(ThemeContext) and throws a descriptive error if used outside the provider<App /> in <ThemeProvider> inside main.tsxuseState from Navigation/ThemeToggle and replace it with const { dark, toggleDark } = useTheme()Start with the form, then add boundaries. React Hook Form touches more of your code — get it working first, then layering Error Boundaries on top is a ten-minute job.
textarea works the same as input with React Hook Form: <textarea {...register('message', { required: '...' })} />. Just make sure there's no value prop left over from the useState version.
The email pattern rule looks like this:
email: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email address',
}
Keep useNavigate where it is. You already call it after form submission. With React Hook Form, it moves into the onSubmit function you pass to handleSubmit — the logic is the same, just the location changes slightly.
If a field error isn't showing, check that you've destructured errors from formState: { errors } and not from somewhere else.
Tailwind and error states: text-red-600 and border-red-500 are the standard Tailwind classes for error styling. Add border-red-500 to an input when errors.fieldName is defined to give a visual error state on the field itself, not just the message below it.
Run npm run build before pushing. Vercel will fail if your TypeScript has errors, and it's faster to catch them locally first.
Follow the Assignment submission guide to learn how to submit the assignment
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.