Files
admin/docs/architecture/react-frontend/layers/view.md
T

151 lines
4.6 KiB
Markdown

# 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 (
<div>
<ProjectStatsWidget projectId={projectId} />
<ProjectActivityWidget projectId={projectId} />
<ProjectMembersWidget projectId={projectId} />
</div>
);
};
```
## 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<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`.
```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
<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>
```