Files
admin/docs/apps-architecture/react-frontend/layers/view.md
T
2026-06-25 15:53:49 +03:00

346 lines
9.2 KiB
Markdown

# Views Layer
## Table of Contents
- [Views Layer](#views-layer)
- [Components](#components)
- [Components Purpose](#components-purpose)
- [Components Responsibilities](#components-responsibilities)
- [What Components should NOT do](#what-components-should-not-do)
- [Widgets](#widgets)
- [Widget Purpose](#widget-purpose)
- [Widget Characteristics](#widget-characteristics)
- [Widget Responsibilities](#widget-responsibilities)
- [Forms](#forms)
- [Form Architecture](#form-architecture)
- [Form Logic](#form-logic)
- [Form Validation](#form-validation)
- [Form Values](#form-values)
- [Form Types](#form-types)
- [Submit Functions](#submit-functions)
- [Purpose](#submit-functions-purpose)
- [API Example](#api-example)
- [Responsibilities](#responsibilities)
- [Composed Form Elements](#composed-form-elements)
- [Example: InputSwitch](#example-inputswitch)
- [Usage in a Form](#usage-in-a-form)
## Components
### Components Purpose
Components are reusable UI blocks used to build user interfaces.
They encapsulate presentation and simple UI behavior, but do not contain business logic or data fetching. `UserAvatar`, `ProductBadge`
Components are designed to be small, composable, and reusable across multiple parts of the application.
### Components Responsibilities:
- render UI
- accept props
- handle simple UI state (hover, toggle, open/close)
- compose other components
### What **Components** should NOT do
Components should not contain logic such as:
- API calls
- mutations
- complex state management
- business workflows
## Widgets
### Widget Purpose
Widgets are **self-contained UI blocks that implement a specific piece of functionality**.
Unlike simple components, widgets combine **UI, logic, and interactions** to deliver a complete user-facing feature.
Widgets are typically used to **compose pages** and represent meaningful parts of the interface such as panels, sections, or complex interactive blocks. Widgets should remain modular building blocks.
### Widget Characteristics
A widget usually:
- encapsulates **UI and feature logic**
- composes **multiple components**
- may include **forms, dialogs, or actions**
- can interact with **data**
- exposes a **simple API for pages**
Widgets should hide their internal implementation and provide a **clean interface** to the outside.
### Widget Responsibilities
Widgets may:
- orchestrate UI interactions
- manage local UI state
- connect to features or data hooks
- combine multiple components/forms into a functional block
- may fetch data or rely on data passed in props
- A **widget** can use another **widget** if:
- the nested widget represents a clear functional sub-block
- the parent widget is grouping or orchestrating UI
- the dependency does not create tight coupling
```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
| --- Component.tsx
| --- submit.ts
| --- constants.ts # Optional
| --- schema.ts # Optional validation schema. Good for large forms or multistep forms
| --- index.ts
```
| File | Responsibility |
| --------------- | ---------------------------------- |
| `form.ts` | validation schema, types, defaults |
| `Component.tsx` | UI + react-hook-form integration |
| `submit.ts` | Submission logic |
| `constants.ts` | form IDs, UI constants |
| `index.ts` | public exports |
### Form Logic
`form.ts` contains pure form logic.
It should include:
- validation schema
- form types
- default values
- helper templates
#### Form Validation
```ts
export const validationSchema = z.object({
first_name: z.string().min(3),
last_name: z.string().min(3),
});
```
If you use `i18n` you can create function that returns validation schema.
```ts
function createValidationSchema(
translationFn: TFunction<"translation", undefined>, // Depend on libraty 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
#### Form Values
Default values should be defined using factory functions.
Avoid inline default objects.
```ts
export function createFormValues(): FormValues {
return {
first_name: "",
last_name: "",
};
}
```
```ts
export function toFormValues(data: SomeData): FormValues {
return {
first_name: data?.first_name ?? "",
last_name: data?.last_name ?? "",
};
}
```
#### Form Types
Define types for form values and submit functiuons
```ts
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => void;
```
#### Submit Functions
##### Purpose {#submit-functions-purpose}
The `useSubmit` hook standardizes form submission logic:
- Handles create and update operations
- Integrates with `data/` layer hooks (`useCreate`, `useUpdate`)
- Automatically handles validation errors from the API
- Invalidates lists or detailed queries to refresh UI
- Exposes a unified API for forms
This pattern allows keep submission logic centralized.
##### API Example
```ts
interface Params {
onSuccess?: (id?: number) => void;
}
export const useSubmit = (params?: Params) => {
const onSuccess = params?.onSuccess;
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
const { mutate: updateMutation, isPending: isUpdateending } = useUpdate();
const handleSubmit: SubmitFunc = (values, handlers) => {
if (values.id) {
handleUpdate(values, handlers);
} else {
handleCreate(values, handlers);
}
};
const handleCreate: SubmitFunc = (values, handlers) => {
createMutation(values, {
onSuccess() {
invalidateList();
if (typeof onSuccess === "function") onSuccess();
},
onError(error) {
extractValidationErrors(error).forEach((e) => {
handlers.setError(e.field as keyof typeof values, {
type: "apiError",
message: e.message,
});
});
},
});
};
const handleUpdate: SubmitFunc = (values, handlers) => {
if (!values.id) return;
updateMutation(
{ ...values, id: values.id },
{
onSuccess() {
invalidateList();
if (values.id) invalidateDetailed(values.id);
if (typeof onSuccess === "function") {
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 || isUpdateending,
};
};
```
##### Responsibilities
1. Determine operation type
- If values.id exists -> update
- Otherwise -> create
2. Call the appropriate mutation
- `createMutation` or `updateMutation` from `data` layer
- Hooks handle API calls internally
3. Handle success
- Invalidate queries (list/detail)
- Trigger optional `onSuccess` callback
4. Handle errors
- Extract validation errors from API
- Set errors on form fields using `handlers.setError`
5. Expose state
- `submit` -> function to call from form
- `isSubmitting` -> combines pending state from both mutations
#### Composed Form Elements
To avoid repetitive boilerplate code when building forms, we encourages creating composed form elements that integrate React Hook Form with shared UI components.
- Combines label, input, validation message, and layout in one reusable component.
- Can encapsulate any input type: text, switch, select, date, textarea, etc.
- Works seamlessly with FormProvider for context propagation.
##### Example: InputSwitch
InputSwitch wraps a Switch component into a reusable form field:
```tsx
<InputSwitch
name="notificationsEnabled"
label="Enable notifications"
description="Toggle notifications on/off"
required
/>
```
Key points:
- Uses `useFormContext` to access form control.
- Wraps `FormField`, `FormItem`, `FormLabel`, `FormMessage`, and `FormControl`.
- Handles required and disabled states.
- This allows you to reuse the same input type across forms without writing repetitive boilerplate.
##### Usage in a Form
```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>
```