Focus: useRef, custom hooks, useTransition
Your portfolio is deployed, functional, and styled. This week's additions are smaller in scope than previous weeks โ but they're the kind of details that separate a project that works from one that feels considered. Auto-focusing form fields, syncing the browser tab title to the current page, and simplifying loading state with React 19's useTransition are all low-effort, high-polish improvements that make the portfolio noticeably more professional.
This assignment assumes your portfolio is the deployed Vercel application from week 9, with:
/, /projects, and /contactWhen a user navigates to /contact, the first input field โ the name field โ should be focused automatically so they can start typing without clicking first. This is a small accessibility improvement: users navigating by keyboard or arriving with clear intent don't have to take an extra action.
In your ContactPage, create a ref for the name input and focus it when the component mounts:
import { useRef, useEffect } from 'react';
const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => {
nameRef.current?.focus();
}, []);
React Hook Form registers inputs using its own ref internally, so attaching your own ref requires a small adjustment. Use useForm's register alongside a manual ref callback, or switch the name field to use Controller so you can attach your own ref directly.
The cleanest approach is to get the ref from register using the returned ref property:
const { ref: nameRegisterRef, ...nameRegisterRest } = register('name', {
required: 'Name is required',
minLength: { value: 2, message: 'Name is too short' },
});
// Combine React Hook Form's ref with your own
const setNameRef = (el: HTMLInputElement | null) => {
nameRegisterRef(el); // React Hook Form needs this
(nameRef as React.MutableRefObject<HTMLInputElement | null>).current = el;
};
Then on the input:
<input ref={setNameRef} {...nameRegisterRest} />
Confirm it works: navigate to /contact and check that the cursor is in the name field immediately, without clicking.
<aside> ๐ก
Accessibility note
Auto-focusing is appropriate here because the entire purpose of the /contact page is to fill in the form โ unlike a general page where focus-on-mount would be disorienting. Always consider whether auto-focus helps or distracts before applying it.
</aside>
Right now, every page of your portfolio probably shows the same tab title. Update it per page so that navigating to /projects shows "Projects โ Your Name" in the browser tab, and so on.
Create a custom hook in src/hooks/useDocumentTitle.ts:
import { useEffect } from 'react';
export function useDocumentTitle(title: string) {
useEffect(() => {
const previous = document.title;
document.title = title;
// Restore the original title when the component unmounts
return () => {
document.title = previous;
};
}, [title]);
}
Call it at the top of each page component:
// HomePage.tsx
useDocumentTitle('Home โ Your Name');
// ProjectsPage.tsx
useDocumentTitle('Projects โ Your Name');
// ContactPage.tsx
useDocumentTitle('Contact โ Your Name');
Open each page in the browser and confirm the tab title updates as you navigate. Then open a new tab on any page and confirm the title is correct from the first render โ not just after navigating from another page.
In week 9 you used isSubmitting from React Hook Form's formState to disable the submit button while the form is processing. React 19's useTransition gives you a complementary tool for the same pattern โ and it can wrap any async operation, not just form submission.
This is an opportunity to see both approaches and understand the difference. isSubmitting from React Hook Form tracks the state of the form's own async submission. useTransition's isPending tracks any async operation you explicitly start with startTransition.
Wrap your onSubmit handler with startTransition:
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
function onSubmit(data: ContactFormData) {
startTransition(async () => {
// Your existing simulated API call
await new Promise((resolve) => setTimeout(resolve, 1500));
reset();
navigate('/');
});
}
Update the submit button to use isPending alongside (or instead of) isSubmitting:
<button type="submit" disabled={isPending || isSubmitting}>
{isPending ? 'Sending...' : 'Send message'}
</button>
Confirm the button disables correctly during submission and that the form resets and redirects after a successful submission.
Note: In a real production app you would pick one or the other โ isSubmitting from React Hook Form is usually sufficient for forms. The goal here is to practise useTransition in a realistic context, not to use both simultaneously in production code.
Before submitting, run through this checklist:
Auto-focus
/contact focuses the name input automaticallyDocument title
useDocumentTitle lives in src/hooks/useDocumentTitle.ts and is imported into each pageuseTransition
isPending is trueGeneral
npm run build completes with no TypeScript errorsref={setNameRef}) is the most reliable approach.useDocumentTitle should restore the previous title on unmount. This means if a user has a custom browser tab title set (unlikely but possible), navigating away from your page restores it rather than leaving your page's title behind.useTransition requires React 19. Your Vite project should already be on React 19 if you created it recently. If useTransition doesn't accept async functions, check your React version in package.json โ you may be on React 18, in which case skip requirement 3 or upgrade with npm install react@19 react-dom@19.useEffect only runs after mount โ if the title is correct on navigation but wrong on direct load, check that you're calling useDocumentTitle unconditionally at the top of each page component.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.