Form Utils
A set of utilities for working with forms and Zod schemas.
Installation
npx shadcn@latest add https://shuip.xyz/r/form-utils.json
Preview
Form Values
{ "name": "John Doe", "age": 25, "address": {}, "status": "INACTIVE" }
Changed Fields
A collection of TypeScript utilities to efficiently manage forms, detect changes, and generate default values from Zod schemas.
getChangedFields
Detects changed fields between two objects using deep comparison.
export function getChangedFields<T extends Record<string, unknown>>(oldObject: T | null | undefined,newObject: T | null | undefined,): Partial<T>
Parameters
oldObject
: The original object (can be null or undefined)newObject
: The new object to compare (can be null or undefined)
Return
A partial object containing only the fields that have changed.
Examples
// Detect simple changesconst oldData = { name: "John", age: 25 };const newData = { name: "John", age: 30 };const changes = getChangedFields(oldData, newData);// Result: { age: 30 }// Handle nested objectsconst oldForm = {user: { name: "John", email: "john@example.com" },settings: { theme: "dark" }};const newForm = {user: { name: "Jane", email: "john@example.com" },settings: { theme: "dark" }};const changes = getChangedFields(oldForm, newForm);// Result: { user: { name: "Jane", email: "john@example.com" } }// Handle edge casesgetChangedFields(null, { name: "John" }); // { name: "John" }getChangedFields({ name: "John" }, null); // { name: undefined }
Usage with React Hook Form
const form = useForm<FormData>({defaultValues: initialData});const handleSubmit = (data: FormData) => {// Detect only modified fieldsconst changedFields = getChangedFields(form.formState.defaultValues, data);// Send only changes to the APIawait updateUser(changedFields);};
getZodDefaultValues
Generates complete default values from a Zod schema, with support for nested objects and TypeScript autocompletion.
export function getZodDefaultValues<T extends z.ZodObject<z.ZodRawShape>>(schema: T,data?: Partial<z.infer<T>>): z.infer<T>
Parameters
schema
: The Zod schema from which to generate default valuesdata
: Optional data to override certain default values
Return
A typed object with all appropriate default values.
Examples
const userSchema = z.object({name: z.string(),age: z.number(),address: z.object({street: z.string(),city: z.string(),zip: z.string(),}),role: z.enum(['admin', 'user']),createdAt: z.date().optional(),});// Generate default valuesconst defaultValues = getZodDefaultValues(userSchema);/* Result:{name: "",age: 0,address: {street: "",city: "",zip: ""},role: undefined,createdAt: undefined}*/// With pre-filled dataconst defaultValues = getZodDefaultValues(userSchema, {name: "John Doe",age: 25});/* Result:{name: "John Doe",age: 25,address: {street: "",city: "",zip: ""},role: undefined,createdAt: undefined}*/
Usage with React Hook Form
const form = useForm<z.infer<typeof userSchema>>({resolver: zodResolver(userSchema),defaultValues: getZodDefaultValues(userSchema, {name: 'John Doe',age: 25,}),});// Autocompletion works perfectly!const values = form.getValues();values.name // ✅ stringvalues.address.street // ✅ string
zodTypeDefaultValue
Internal utility function that generates an appropriate default value for a given Zod type.
export function zodTypeDefaultValue(key: z.ZodTypeAny): unknown
Supported Types
z.ZodString
→""
z.ZodNumber
→0
z.ZodBoolean
→false
z.ZodArray
→[]
z.ZodObject
→ Object with all fields initialized recursivelyz.ZodOptional
→undefined
z.ZodNullable
→null
z.ZodDate
→undefined
z.ZodEnum
→undefined
z.ZodNativeEnum
→undefined
Example
// Direct usage (rare)zodTypeDefaultValue(z.string()); // ""zodTypeDefaultValue(z.number()); // 0zodTypeDefaultValue(z.boolean()); // false// For nested objects (automatic via getZodDefaultValues)zodTypeDefaultValue(z.object({name: z.string(),age: z.number()}));// { name: "", age: 0 }
Complete Example
Here's a complete example showing how to use these utilities together:
'use client';import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { z } from 'zod';import { getChangedFields, getZodDefaultValues } from '@/lib/form-utils';const userSchema = z.object({name: z.string(),age: z.number(),address: z.object({street: z.string().optional(),city: z.string().optional(),zip: z.string().optional(),}),role: z.enum(['admin', 'user']).optional(),});type UserForm = z.infer<typeof userSchema>;export function UserFormExample() {const [changedFields, setChangedFields] = useState<Partial<UserForm>>();const form = useForm<UserForm>({resolver: zodResolver(userSchema),defaultValues: getZodDefaultValues(userSchema, {name: 'John Doe',age: 25,}),});const handleSubmit = (data: UserForm) => {// Detect changesconst changes = getChangedFields(form.formState.defaultValues, data);setChangedFields(changes);// API call with only modified fieldsconsole.log('Modified fields:', changes);// Reset form with new valuesform.reset(data);};return (<form onSubmit={form.handleSubmit(handleSubmit)}>{/* Your form fields */}<div className="debug-info"><h3>Current values:</h3><pre>{JSON.stringify(form.getValues(), null, 2)}</pre><h3>Changed fields:</h3><pre>{JSON.stringify(changedFields, null, 2)}</pre></div></form>);}
Benefits
- Type Safety: Complete TypeScript autocompletion
- Performance: Efficient change detection with deep comparison
- Flexibility: Support for nested objects and complex types
- Integration: Works perfectly with React Hook Form and Zod
- Edge Cases: Robust handling of null/undefined values
Examples
Default
Form Values
{ "name": "John Doe", "age": 25, "address": {}, "status": "INACTIVE" }
Changed Fields
Props
No props