4.6 KiB
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:
export const ProjectOverviewWidget = ({ projectId }: Props) => {
return (
<div>
<ProjectStatsWidget projectId={projectId} />
<ProjectActivityWidget projectId={projectId} />
<ProjectMembersWidget projectId={projectId} />
</div>
);
};
Forms
Form Architecture
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.
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:
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
i18nsingleton 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:
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:
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => 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.
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.
<FormProvider {...form}>
<Component.FormElement
id={MAIN_FORM_ID}
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
>
<Component.FormGrid columns={2}>
<Component.GridElement colSpan={1}>
<Component.InputSwitch
name="notificationsEnabled"
required
label="Enable notifications"
/>
</Component.GridElement>
</Component.FormGrid>
</Component.FormElement>
</FormProvider>