diff --git a/docs/docs/guide/react.mdx b/docs/docs/guide/react.mdx index 2984e13..117c269 100644 --- a/docs/docs/guide/react.mdx +++ b/docs/docs/guide/react.mdx @@ -457,9 +457,796 @@ Use controlled inputs when all three conditions above are not met. There are many third party form libraries that can help you manage forms in a more declarative way. For example, [React Hook Form](https://react-hook-form.com/) and [Formik](https://formik.org/). - - Discuss and write down when to use third party form libraries. - +#### When to use third party form libraries + +1. Real time / instant validation of input values. + +There are cases where an input value is required to be validated immediately without having to wait for the user to submit the form. For example: + +```tsx title="email-form.tsx" lineNumbers +"use client"; + +import { useForm } from "react-hook-form"; +import { useState } from "react"; + +// Server function to check if email exists +async function checkEmailExists(email: string): Promise { + const response = await fetch("/api/check-email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + const data = await response.json(); + return data.exists; +} + +export default function EmailForm() { + const { + register, + handleSubmit, + formState: { errors, isValidating }, + setError, + clearErrors, + } = useForm(); + const [isCheckingEmail, setIsCheckingEmail] = useState(false); + + const validateEmail = async (email: string) => { + if (!email) return "Email is required"; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return "Invalid email format"; + } + + setIsCheckingEmail(true); + try { + const exists = await checkEmailExists(email); + if (exists) { + return "This email is already registered"; + } + return true; + } catch (error) { + return "Failed to validate email"; + } finally { + setIsCheckingEmail(false); + } + }; + + const onSubmit = async (data: any) => { + // Send the payload to the server + await fetch("/api/check-email", { + method: "POST", + body: JSON.stringify(data), + }); + }; + + return ( +
+
+ + + {isCheckingEmail && ( +

Checking email...

+ )} + {errors.email && ( +

+ {errors.email.message as string} +

+ )} +
+ +
+ ); +} +``` + +The example above validates if the email is not duplicated with the existing emails in the database. This is a good use case for third party form libraries, as it can show the validation error UI feedbacks such as red borders, error message, etc. immediately without having to wait for the user to submit the form. + +2. Conditional rendering of input fields. + +There are cases where you want to conditionally render an input field based on the value of another input field. For example, take a look at this case of activity creation form: + +There is a field called `type`. The value of `type` is either "event", "regular", and "project" + +- If the value of `type` is "event", then there should be an array of fields for the event dates. +- If the value of `type` is "regular", then there should be an array of fields for the regular schedules per day, along with the start and end time of the whole activity period. +- If the value of `type` is "project", then there should be a field for the start and end date of the project. + +```tsx title="activity-form.tsx" lineNumbers +"use client"; + +import { useForm, useFieldArray } from "react-hook-form"; + +type ActivityType = "event" | "regular" | "project"; + +interface ActivityFormData { + name: string; + type: ActivityType; + // Event fields + eventDates?: { date: string }[]; + // Regular fields + regularSchedules?: { + day: string; + startTime: string; + endTime: string; + }[]; + activityStartDate?: string; + activityEndDate?: string; + // Project fields + projectStartDate?: string; + projectEndDate?: string; +} + +export default function ActivityForm() { + const { register, control, watch, handleSubmit } = useForm({ + defaultValues: { + name: "", + type: "event", + eventDates: [{ date: "" }], + regularSchedules: [{ day: "Monday", startTime: "", endTime: "" }], + }, + }); + + const activityType = watch("type"); + + const { + fields: eventDateFields, + append: appendEventDate, + remove: removeEventDate, + } = useFieldArray({ + control, + name: "eventDates", + }); + + const { + fields: scheduleFields, + append: appendSchedule, + remove: removeSchedule, + } = useFieldArray({ + control, + name: "regularSchedules", + }); + + const onSubmit = (data: ActivityFormData) => { + console.log("Form submitted:", data); + // Remove the unnecessary fields from the payload + switch (activityType) { + case "event": + return { + name: data.name, + eventDates: data.eventDates, + }; + case "regular": + return { + name: data.name, + regularSchedules: data.regularSchedules, + activityStartDate: data.activityStartDate, + activityEndDate: data.activityEndDate, + }; + case "project": + return { + name: data.name, + projectStartDate: data.projectStartDate, + projectEndDate: data.projectEndDate, + }; + } + + // Send the payload to the server + await fetch("/api/activities", { + method: "POST", + body: JSON.stringify(payload), + }); + }; + + return ( +
+
+ + +
+ +
+ + +
+ + {/* Conditional fields based on activity type */} + {activityType === "event" && ( +
+ + {eventDateFields.map((field, index) => ( +
+ + +
+ ))} + +
+ )} + + {activityType === "regular" && ( +
+
+
+ + +
+
+ + +
+
+ +
+ + {scheduleFields.map((field, index) => ( +
+
+ + + +
+ +
+ ))} + +
+
+ )} + + {activityType === "project" && ( +
+
+ + +
+
+ + +
+
+ )} + + +
+ ); +} +``` + +This is a good use case for third party form libraries, as it can handle the conditional rendering of input fields and preprocess the payload data before sending it to the server. + +3. Dynamic form arrays + +There are cases where you want to dynamically add or remove input fields in a form. For example, take a look at this case of inputting divisions in an activity creation form: + +a. There is a field called `divisions`. +b. The value of `divisions` is an array of objects. +c. Each object has a `name`, `description`, and `number_of_volunteers_needed` fields. +d. The user can add or remove divisions by clicking a button. + +```tsx title="divisions-form.tsx" lineNumbers +"use client"; + +import { useForm, useFieldArray } from "react-hook-form"; + +interface Division { + name: string; + description: string; + number_of_volunteers_needed: number; +} + +interface DivisionsFormData { + activityName: string; + divisions: Division[]; +} + +export default function DivisionsForm() { + const { + register, + control, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + activityName: "", + divisions: [ + { + name: "", + description: "", + number_of_volunteers_needed: 0, + }, + ], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "divisions", + }); + + const onSubmit = async (data: DivisionsFormData) => { + // Preprocess the payload before sending to server + const payload = { + activityName: data.activityName, + divisions: data.divisions.filter( + (division) => + division.name.trim() !== "" && + division.number_of_volunteers_needed > 0 + ), + }; + + console.log("Processed payload:", payload); + + // Send to server + await fetch("/api/activities", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + }; + + return ( +
+
+ + + {errors.activityName && ( +

+ {errors.activityName.message} +

+ )} +
+ +
+
+

Divisions

+ +
+ + {fields.map((field, index) => ( +
+
+

+ Division {index + 1} +

+ +
+ +
+ + + {errors.divisions?.[index]?.name && ( +

+ {errors.divisions[index]?.name?.message} +

+ )} +
+ +
+ +