# Views Layer ## Components Reusable UI blocks: render UI, accept props, handle simple UI state (hover, toggle, open/close), compose other components. `UserAvatar`, `ProductBadge`. No API calls, mutations, complex state management, or business workflows. ## Widgets Self-contained UI blocks that implement a complete piece of functionality — panels, sections, complex interactive blocks used to compose pages. A widget combines UI, logic, and interactions: it composes multiple components, may include forms, dialogs, or actions, may fetch data via data hooks or take it from props, and exposes a simple API to pages while hiding its internals. A widget can use another widget when the nested widget is a clear functional sub-block, the parent is grouping or orchestrating UI, and the dependency stays loose: ```ts export const ProjectOverviewWidget = ({ projectId }: Props) => { return (
); }; ``` ## Forms ### Form Architecture ```plaintext view/forms/SomeForm/ | --- form.ts # validation schema, types, default values | --- Component.tsx # UI + react-hook-form integration | --- submit.ts # submission logic | --- constants.ts # Optional: form IDs, UI constants | --- schema.ts # Optional validation schema. Good for large or multistep forms | --- index.ts # public exports ``` ### Form Logic `form.ts` contains pure form logic: validation schema, form types, default values, helper templates. ```ts export const validationSchema = z.object({ first_name: z.string().min(3), last_name: z.string().min(3), }); ``` With `i18n`, create a function that returns the schema: ```ts function createValidationSchema( translationFn: TFunction<"translation", undefined>, // Depend on library you use ) { return z.object({ first_name: z.string({ message: translationFn("required") }), last_name: z.string({ message: translationFn("required") }), }); } ``` > [! WARNING] > Dont include `i18n` singleton last_name: z.string().min(1, i18n.t('required')) > It will not translate errors when language state change Default values come from factory functions, not inline objects: ```ts export function createFormValues(): FormValues { return { first_name: "", last_name: "" }; } export function toFormValues(data: SomeData): FormValues { return { first_name: data?.first_name ?? "", last_name: data?.last_name ?? "", }; } ``` Types for values and submit functions: ```ts export type FormValues = z.infer; export type SubmitFunc = ( values: FormValues, handlers: UseFormReturn, ) => void; ``` ### Submit Functions A `useSubmit` hook centralizes submission: it picks create vs update by `values.id`, calls the `data/` layer mutations, invalidates list/detail queries on success, maps API validation errors onto form fields, and exposes `submit` + `isSubmitting`. ```ts export const useSubmit = (params?: { onSuccess?: (id?: number) => void }) => { const { mutate: createMutation, isPending: isCreatePending } = useCreate(); const { mutate: updateMutation, isPending: isUpdatePending } = useUpdate(); const handleSubmit: SubmitFunc = (values, handlers) => { const mutation = values.id ? updateMutation : createMutation; mutation(values, { onSuccess() { invalidateList(); if (values.id) invalidateDetailed(values.id); params?.onSuccess?.(values.id ?? undefined); }, onError(error) { extractValidationErrors(error).forEach((e) => { handlers.setError(e.field as keyof typeof values, { type: "apiError", message: e.message, }); }); }, }); }; return { submit: handleSubmit, isSubmitting: isCreatePending || isUpdatePending, }; }; ``` ### Composed Form Elements To avoid form boilerplate, wrap React Hook Form + shared UI into composed inputs. Each combines label, input, validation message, and layout, works with `FormProvider` via `useFormContext`, and can encapsulate any input type: text, switch, select, date, textarea, etc. ```tsx onSubmit(values, form))} > ```