9.2 KiB
Views Layer
Table of Contents
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
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
| --- 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
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.
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
i18nsingleton 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.
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 ?? "",
};
}
Form Types
Define types for form values and submit functiuons
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => void;
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
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
- Determine operation type
- If values.id exists -> update
- Otherwise -> create
- Call the appropriate mutation
createMutationorupdateMutationfromdatalayer- Hooks handle API calls internally
- Handle success
- Invalidate queries (list/detail)
- Trigger optional
onSuccesscallback
- Handle errors
- Extract validation errors from API
- Set errors on form fields using
handlers.setError
- Expose state
submit-> function to call from formisSubmitting-> 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:
<InputSwitch
name="notificationsEnabled"
label="Enable notifications"
description="Toggle notifications on/off"
required
/>
Key points:
- Uses
useFormContextto access form control. - Wraps
FormField,FormItem,FormLabel,FormMessage, andFormControl. - Handles required and disabled states.
- This allows you to reuse the same input type across forms without writing repetitive boilerplate.
Usage in a Form
<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>