Building a Nutritional Tracker: Part 5 - The Registration Form
3 min
Part 5: The Registration Form — Nutrition Tracker
Introduction
Up to this point, our app validates data, persists it in the browser, and tests it automatically. The next challenge was to connect all the pieces in the main registration form, where the user enters their daily nutritional intake.
What does the form integrate?
- Validation: Uses the Zod schema and TypeScript to never accept invalid data.
- Persistence: Automatically saves records in localStorage.
- React UI: Componentization, states, visual feedback.
- React Hook Form: Manages the form lifecycle and efficient communication with validators.
Basic component structure
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { RegisterInputSchema } from "@/lib/schemas/register.schema";
import { saveRegister } from "@/lib/storage/localStorage";
function RegistrationForm() {
const { register, handleSubmit, formState: { errors }, reset } = useForm({
resolver: zodResolver(RegisterInputSchema),
defaultValues: {
// ...initial values
},
});
const onSubmit = (data) => {
const result = saveRegister(data);
if (result.success) {
reset();
// Show success feedback
} else {
// Show error feedback and field messages
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("food")} />
{errors.food && <span>{errors.food.message}</span>}
{/* repeat for the rest of the fields */}
<button type="submit">Save</button>
</form>
);
}
export default RegistrationForm;Explanations:
- The form receives pre-configured functions from
react-hook-form. - On submit, it validates with Zod and saves to localStorage only if the data is valid.
- Feedback shows messages for incorrect fields and a global message on save.
Interaction diagram: Form Submission
sequenceDiagram
participant User
participant Form
participant Zod
participant Storage
User->>Form: Fills fields and submits
Form->>Zod: Validates data
Zod-->>Form: Valid?
alt success
Form->>Storage: Saves record
Storage-->>Form: status success/error
Form-->>User: Shows success feedback
else error
Zod-->>Form: Returns errors
Form-->>User: Shows field and general feedback
end
Advantages of this design
- Scalable: Changes in the data model are automatically reflected in the form.
- Type safe: All data is strictly typed and validated without duplication.
- Clear UX: The user gets clear feedback and invalid fields are highlighted.
- Modularity: Separating validation, persistence, and UI logic increases maintainability.
How do we ensure the form works? Tests and practical examples
Here are some examples and concepts from the test suite that validate the real behavior of the form:
Test: The form blocks invalid data
Why? This prevents unwanted saves and shows clear errors to the user.
it("prevents submission if required fields are empty", async () => {
render(<RegistrationForm />);
fireEvent.click(screen.getByRole("button", { name: /save record/i }));
await waitFor(() => {
expect(screen.getByText(/user.*required/i)).toBeInTheDocument();
expect(screen.getByText(/food.*required/i)).toBeInTheDocument();
});
});Test: Visual feedback on save
Why? The user needs to know if their data was saved or if there was an error.
it("shows success message on save", async () => {
render(<RegistrationForm />);
// ...fill and submit...
fireEvent.click(screen.getByRole("button", { name: /save record/i }));
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(/saved/i);
});
});Test: Persistence and field reset
Why? Prevents duplicate entries, helps UX, and ensures storage works with the form.
it("saves record and resets fields keeping user", async () => {
render(<RegistrationForm />);
// ...fill data, select user...
fireEvent.click(screen.getByRole("button", { name: /save record/i }));
await waitFor(() => {
expect(screen.getByLabelText(/food/i).value).toBe("");
expect(screen.getByLabelText(/user/i).value).not.toBe("");
});
});Test: Correct data reaches storage
Why? Verifies integration between frontend and persistence.
it("persists correctly in localStorage", async () => {
render(<RegistrationForm />);
// ...fill data and submit...
fireEvent.click(screen.getByRole("button", { name: /save record/i }));
await waitFor(() => {
const raw = localStorage.getItem("nutrition-tracker-registers");
expect(raw).toBeTruthy();
const arr = JSON.parse(raw);
expect(Array.isArray(arr)).toBe(true);
expect(arr[arr.length - 1].food).toBe("Apple");
});
});Why test this way?
- Prevents user frustration from unexpected errors.
- Ensures storage always receives correct data.
- On-screen feedback and