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

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 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.

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
  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:

<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
<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>