|
| 1 | +--- |
| 2 | +type: Always |
| 3 | +description: Rules for form component integration patterns in the lambda-curry/forms repository (applies to Remix Hook Form components) |
| 4 | +--- |
| 5 | + |
| 6 | +You are an expert in React Hook Form, Remix Hook Form, Zod validation, and form component architecture for the lambda-curry/forms monorepo. |
| 7 | + |
| 8 | +# Form Component Integration Patterns (Remix Hook Form) |
| 9 | + |
| 10 | +**Note: These patterns apply to Remix Hook Form components in `packages/components/`. For Medusa Forms components using react-hook-form + @medusajs/ui, see the medusa-forms-patterns.mdc rules.** |
| 11 | + |
| 12 | +## Core Principles |
| 13 | +- All form components must integrate seamlessly with Remix Hook Form |
| 14 | +- Use Zod schemas for validation with proper TypeScript inference |
| 15 | +- Follow the wrapper pattern for consistent component composition |
| 16 | +- Maintain separation between UI components and form-aware components |
| 17 | +- Ensure proper error handling and validation feedback |
| 18 | + |
| 19 | +## Required Imports for Form Components |
| 20 | +```typescript |
| 21 | +// Remix Hook Form integration |
| 22 | +import { useRemixFormContext } from 'remix-hook-form'; |
| 23 | +import { zodResolver } from '@hookform/resolvers/zod'; |
| 24 | +import { z } from 'zod'; |
| 25 | + |
| 26 | +// Form components |
| 27 | +import { FormControl, FormDescription, FormLabel, FormMessage } from './form'; |
| 28 | + |
| 29 | +// Base UI components |
| 30 | +import { ComponentName as BaseComponentName } from '../ui/component-name'; |
| 31 | +``` |
| 32 | + |
| 33 | +## Form Schema Pattern |
| 34 | +Always define Zod schemas with proper error messages: |
| 35 | +```typescript |
| 36 | +const formSchema = z.object({ |
| 37 | + fieldName: z.string().min(1, 'Field is required'), |
| 38 | + email: z.string().email('Invalid email address'), |
| 39 | + price: z.string().min(1, 'Price is required'), |
| 40 | +}); |
| 41 | + |
| 42 | +type FormData = z.infer<typeof formSchema>; |
| 43 | +``` |
| 44 | + |
| 45 | +## Wrapper Component Pattern |
| 46 | +Follow this pattern for all form-aware components: |
| 47 | +```typescript |
| 48 | +export type ComponentNameProps = Omit<BaseComponentNameProps, 'control'>; |
| 49 | + |
| 50 | +export function ComponentName(props: ComponentNameProps) { |
| 51 | + const { control } = useRemixFormContext(); |
| 52 | + |
| 53 | + // Merge provided components with default form components |
| 54 | + const defaultComponents = { |
| 55 | + FormControl, |
| 56 | + FormLabel, |
| 57 | + FormDescription, |
| 58 | + FormMessage, |
| 59 | + }; |
| 60 | + |
| 61 | + const components = { |
| 62 | + ...defaultComponents, |
| 63 | + ...props.components, |
| 64 | + }; |
| 65 | + |
| 66 | + return <BaseComponentName control={control} components={components} {...props} />; |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +## Component Composition Pattern |
| 71 | +For UI components that accept form integration: |
| 72 | +```typescript |
| 73 | +export interface ComponentNameProps extends Omit<InputProps, 'prefix' | 'suffix'> { |
| 74 | + control?: Control<FieldValues>; |
| 75 | + name: FieldPath<FieldValues>; |
| 76 | + label?: string; |
| 77 | + description?: string; |
| 78 | + components?: Partial<FieldComponents> & { |
| 79 | + Input?: React.ComponentType<InputProps>; |
| 80 | + }; |
| 81 | + className?: string; |
| 82 | +} |
| 83 | + |
| 84 | +export const ComponentName = ({ |
| 85 | + control, |
| 86 | + name, |
| 87 | + label, |
| 88 | + description, |
| 89 | + className, |
| 90 | + components, |
| 91 | + ...props |
| 92 | +}: ComponentNameProps) => { |
| 93 | + const InputComponent = components?.Input || DefaultInput; |
| 94 | + |
| 95 | + return ( |
| 96 | + <FormField |
| 97 | + control={control} |
| 98 | + name={name} |
| 99 | + render={({ field, fieldState }) => ( |
| 100 | + <FormItem className={className}> |
| 101 | + {label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>} |
| 102 | + <FormControl Component={components?.FormControl}> |
| 103 | + <InputComponent {...field} {...props} /> |
| 104 | + </FormControl> |
| 105 | + {description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>} |
| 106 | + {fieldState.error && ( |
| 107 | + <FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage> |
| 108 | + )} |
| 109 | + </FormItem> |
| 110 | + )} |
| 111 | + /> |
| 112 | + ); |
| 113 | +}; |
| 114 | +``` |
| 115 | + |
| 116 | +## Form Setup Pattern |
| 117 | +Use this pattern for form initialization: |
| 118 | +```typescript |
| 119 | +const ControlledComponentExample = () => { |
| 120 | + const fetcher = useFetcher<{ message: string }>(); |
| 121 | + const methods = useRemixForm<FormData>({ |
| 122 | + resolver: zodResolver(formSchema), |
| 123 | + defaultValues: { |
| 124 | + // Provide sensible defaults |
| 125 | + }, |
| 126 | + fetcher, |
| 127 | + submitConfig: { action: '/', method: 'post' }, |
| 128 | + }); |
| 129 | + |
| 130 | + return ( |
| 131 | + <RemixFormProvider {...methods}> |
| 132 | + <fetcher.Form onSubmit={methods.handleSubmit}> |
| 133 | + {/* Form components */} |
| 134 | + </fetcher.Form> |
| 135 | + </RemixFormProvider> |
| 136 | + ); |
| 137 | +}; |
| 138 | +``` |
| 139 | + |
| 140 | +## Validation Patterns |
| 141 | + |
| 142 | +### Client-Side Validation |
| 143 | +- Use Zod schemas for all form validation |
| 144 | +- Provide clear, user-friendly error messages |
| 145 | +- Validate on blur and submit, not on every keystroke |
| 146 | + |
| 147 | +### Server-Side Validation |
| 148 | +```typescript |
| 149 | +export const action = async ({ request }: ActionFunctionArgs) => { |
| 150 | + const { data, errors } = await getValidatedFormData<FormData>( |
| 151 | + request, |
| 152 | + zodResolver(formSchema) |
| 153 | + ); |
| 154 | + |
| 155 | + if (errors) return { errors }; |
| 156 | + |
| 157 | + // Additional server-side validation |
| 158 | + if (data.username === 'taken') { |
| 159 | + return { |
| 160 | + errors: { |
| 161 | + username: { message: 'Username is already taken' } |
| 162 | + } |
| 163 | + }; |
| 164 | + } |
| 165 | + |
| 166 | + return { message: 'Success!' }; |
| 167 | +}; |
| 168 | +``` |
| 169 | + |
| 170 | +## Error Handling Best Practices |
| 171 | +- Always display field-level errors using FormMessage |
| 172 | +- Handle both client-side and server-side validation errors |
| 173 | +- Provide loading states during form submission |
| 174 | +- Clear errors appropriately when fields are corrected |
| 175 | + |
| 176 | +## Component Naming Conventions |
| 177 | +- Form-aware components: `ComponentName` (e.g., `TextField`, `Checkbox`) |
| 178 | +- Base UI components: `ComponentName` in ui/ directory |
| 179 | +- Props interfaces: `ComponentNameProps` |
| 180 | +- Form schemas: `formSchema` or `componentNameSchema` |
| 181 | + |
| 182 | +## File Organization |
| 183 | +``` |
| 184 | +packages/components/src/ |
| 185 | +├── remix-hook-form/ # Form-aware wrapper components |
| 186 | +│ ├── text-field.tsx |
| 187 | +│ ├── checkbox.tsx |
| 188 | +│ └── index.ts |
| 189 | +└── ui/ # Base UI components |
| 190 | + ├── text-field.tsx |
| 191 | + ├── checkbox.tsx |
| 192 | + └── index.ts |
| 193 | +``` |
| 194 | + |
| 195 | +## Required Exports |
| 196 | +Always export both the component and its props type: |
| 197 | +```typescript |
| 198 | +export { ComponentName }; |
| 199 | +export type { ComponentNameProps }; |
| 200 | +``` |
| 201 | + |
| 202 | +## Performance Considerations |
| 203 | +- Use React.memo for expensive form components |
| 204 | +- Avoid unnecessary re-renders by properly structuring form state |
| 205 | +- Consider field-level subscriptions for large forms |
| 206 | + |
| 207 | +## Accessibility Requirements |
| 208 | +- All form fields must have proper labels |
| 209 | +- Use ARIA attributes for complex form interactions |
| 210 | +- Ensure keyboard navigation works correctly |
| 211 | +- Provide clear error announcements for screen readers |
| 212 | + |
| 213 | +## Testing Integration |
| 214 | +- Form components should work with the existing Storybook testing patterns |
| 215 | +- Test both valid and invalid form states |
| 216 | +- Verify server-side validation integration |
| 217 | +- Test component composition and customization |
| 218 | + |
| 219 | +Remember: Form components are the core of this library. Every form component should be intuitive, accessible, and integrate seamlessly with the Remix Hook Form + Zod validation pattern. |
0 commit comments