Initial commit

This commit is contained in:
Igor Martyniuk
2026-06-25 15:53:49 +03:00
commit 1bb0b7e68a
1517 changed files with 218469 additions and 0 deletions
@@ -0,0 +1,260 @@
# Data Layer
## Table of Contents
- [Data Layer](#data-layer)
- [Purpose](#purpose)
- [Responsibilities](#responsibilities)
- [Data Layer Organization Approaches](#data-layer-organization-approaches)
- [By Module](#by-module)
- [By Entity](#by-entity)
- [Data Flow](#data-flow)
- [DTO Types](#1-dto-types)
- [API Layer](#2-api-layer)
- [Mapper](#3-mapper)
- [Mapper Testing](#mapper-testing)
- [Query Hooks](#4-query-hooks)
- [Pagination](#pagination)
## Purpose
The `data` layer is responsible for **all data access and data transformation** in the application.
It is the only layer that knows:
- how to call APIs
- how to map API responses (DTOs) to application entities
- how to handle caching, queries, and mutations
Higher layers (`forms`, `widgets`, `pages`) **do not interact with APIs directly**.
Instead, they consume **hooks provided by the data layer**.
## Responsibilities
The `data/` layer is responsible for:
- performing **API calls**
- defining **DTO types (Data Transfer Objects)**
- converting DTOs → **entities** using **mappers**
- providing **React Query hooks**
- handling **side effects** (requests, caching, errors)
- defining **API-related types**
## Data Layer Organization Approaches
```plaintext
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces
|
| --- pagination/
| | --- dto.ts
| | --- mapper.ts
| | --- types.ts
```
There are two main ways to structure the `data` layer:
- By Module — grouped by type of file (API, hooks, DTOs, mappers, types)
- By Entity — grouped by business feature/entity (user, product, message)
| Approach | Best For | Pros | Cons |
| --------- | ------------------------------- | -------------------------------------------- | ---------------------------------------------- |
| By Module | Small apps, simple data layers | Easy to find all API/hooks/DTOs in one place | Files for one feature scattered; hard to scale |
| By Entity | Medium/large apps, scalable CRM | Encapsulated, modular, easier to maintain | Harder to see all APIs/hooks globally |
### By Module
```plaintext
data/
| --- api/ # API request functions
| | --- userAPI.ts
| | --- productAPI.ts
| | --- messageAPI.ts
|
| --- hooks/ # React Query hooks or other data hooks
| | --- useUserQuery.ts
| | --- useCreateProductMutation.ts
| | --- useMessages.ts
|
| --- dto/ # API DTO types
| | --- userDTO.ts
| | --- productDTO.ts
| | --- messageDTO.ts
|
├ --- mappers/ # DTO → entity mappers
| | --- userMapper.ts
| | --- productMapper.ts
|
| --- types/ # API types/interfaces
```
### By Entity
```plaintext
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces (optional)
|
| --- product/
| | --- api.ts
| | --- hooks.ts
| | --- dto.ts
| | --- mapper.ts
| | --- types.ts
```
## Data Flow
### 1. DTO Types
```ts
export type UserDTO = {
id: number;
full_name: string;
email_address: string;
};
export type CreateUserDTO = {
full_name: string;
email_address: string;
};
```
### 2. API Layer
API functions or class methods perform HTTP requests. API always returns DTO objects.
```ts
export class UserApi {
private http: HttpClient;
async getDetailed(id: number): Promise<UserDTO> {
return this.http.get(`/users/${id}`);
}
async getList(): Promise<UserDTO[]> {
return this.http.get(`/users`);
}
async create(dto: CreateUserDTO): Promise<UserDTO> {
return this.http.post(`/users`, dto);
}
}
```
### 3. Mapper
Mappers convert `DTO` to `Entity`
```ts
import { User } from "@/entities/user";
export class UserMapper {
toEntity(dto: UserDTO): User {
return {
id: dto.id,
name: dto.full_name,
email: dto.email_address,
};
}
toEntities(dtos: UserDTO[]): User[] {
return dtos.map((dto) => this.toEntity(dto));
}
toPaginated(dto: PaginatedDataDTO<UserDTO[]>): PaginatedResult<User[]> => {
return {
meta: this.paginationMetaMapper.toMeta(dto.meta),
data: this.toEntities(dto.data),
};
};
}
```
### Mapper Testing
Mappers are part of the data layer contract and should have unit tests.
Mapper tests are required when a mapper:
- renames fields from API DTOs to app entities
- normalizes values, paths, dates, sizes, statuses, enums, or defaults
- maps nested DTOs through other mappers
- maps arrays, paginated data, or optional API fields
- protects the app from backend response shape changes
These tests should focus only on transformation behavior. Do not mock React, render components, or call real APIs in mapper tests.
Recommended test coverage:
- `toEntity` maps a representative DTO to the expected entity
- `toEntities` maps arrays consistently
- optional or missing DTO fields use the correct defaults
- nested mappers preserve the expected entity shape
- enum/status casts are covered with expected values
For apps that keep tests outside production source, use a test folder at the app root:
```plaintext
apps/admin/
├── src/
│ └── core/FileManager/data/mappers/entryMapper.ts
└── tests/
└── unit/core/FileManager/data/mappers/entryMapper.test.ts
```
### 4. Query Hooks
Hooks combine API + mapper + React Query.
```ts
export const useUserDetailed = (id: number) => {
const api = new UserApi(http);
const mapper = new UserMapper();
return useQuery({
queryKey: ["user", id],
queryFn: async () => {
const dto = await api.getDetailed(id);
return mapper.toEntity(dto);
},
});
};
```
### Pagination
Pagination is not **entity-specific**, so it should live in a shared data module.
```ts
export type PaginatedMetaDTO = {
total: number;
perPage: number;
currentPage: number;
lastPage: number;
};
export type PaginatedDTO<T> = {
data: T;
meta: PaginatedMetaDTO;
};
export class PaginationMapper {
toMeta(dto: PaginatedMetaDTO) {
return {
total: dto.total,
perPage: dto.perPage,
currentPage: dto.currentPage,
lastPage: dto.lastPage,
};
}
}
```
@@ -0,0 +1,53 @@
# Entities
## Purpose
The `entities` layer defines the **core domain models of the application**.
Entities represent **business objects** used throughout the system, such as `User`, `Product`, `Project`, or `Message`.
This layer contains **pure type definitions** that describe how data is structured inside the application.
Entities are **stable, reusable models** that can be used across any layer.
```plaintext
entities/
| --- user.ts # { id: string; name: string; email: string }
| --- product.ts # { id: string; name: string; price: number }
| --- message.ts # { id: string; content: string; createdAt: string }
| --- enums.ts # { UserRole, ProductCategory }
```
or
```plaintext
entities/
| --- user/
| | --- interfaces.ts # { id: string; name: string; email: string }
| | --- types.ts # { id: string; name: string; email: string }
| | --- enums.ts # { UserRole }
| | --- rules.ts # MIN_ALLOWED_AGE = 18
| | --- index.ts # export all entity types
| --- product.ts # { id: string; name: string; price: number }
| --- message.ts # { id: string; content: string; createdAt: string }
```
## Responsibilities
The entities layer is responsible for:
- defining **domain models**
- defining **entity-related enums or constants**
- providing **shared types used across the application**
```ts
export type User = {
id: number;
name: string;
email: string;
};
export enum UserRole {
ADMIN = "admin",
SIMLPE_JACK = "simple_jack",
}
```
@@ -0,0 +1,106 @@
# Infrastructure Layer
## Table of Contents
- [Purpose](#purpose)
- [Responsibilities](#responsibilities)
- [Recommended Structure](#recommended-structure)
- [Rules](#rules)
- [Examples](#examples)
## Purpose
The `infrastructure/` layer contains app-wide technical adapters and integration helpers.
Use this layer for code that connects the frontend application to technical systems or shared runtime services, not for feature-specific business logic.
## Responsibilities
The `infrastructure/` layer is responsible for:
- HTTP clients and request/response interceptors
- React Query shared helpers, response types, query key helpers, and error normalization
- storage, browser, or runtime adapters
- shared formatting/service adapters such as date or name formatting services
- app-wide technical configuration wrappers
## Recommended Structure
```plaintext
infrastructure/
├── axios/
│ ├── index.ts # configured axios client
│ ├── interceptors.ts # request/response interceptors
│ └── axios.d.ts # axios type augmentation
│
├── reactQuery/
│ ├── index.ts
│ ├── errors.ts # query error handling helpers
│ ├── helpers.ts # query key and pagination helpers
│ ├── queryParams.ts # table/query-param mapping helpers
│ └── types.ts # shared API response/query types
│
├── IntlDate.ts # date formatting service adapter
└── IntlName.ts # name formatting service adapter
```
## Rules
1. Keep infrastructure feature-agnostic.
- No module-specific business rules
- No page or component composition
- No entity mapping that belongs in a module `data/mapper`
2. API modules should use infrastructure clients instead of creating their own clients.
```ts
import { axiosClient } from "#infrastructure/axios";
```
3. React Query helpers that are shared across modules belong here.
Examples: query key builders, list response types, pagination helpers, error extraction helpers.
4. UI layers must not call raw HTTP clients directly.
View and page code should call data-layer hooks. Direct `#infrastructure/axios` usage belongs in API/data modules, not in components.
5. Generic formatting adapters may be consumed by UI when they are not tied to a feature.
Examples: `IntlDate`, `IntlName`.
6. Prefer dependency injection for app-state coupling.
If an interceptor needs auth or query-client behavior, keep that coupling narrow and documented. Avoid importing random feature modules into infrastructure.
## Examples
API modules use the configured HTTP client:
```ts
import { axiosClient } from "#infrastructure/axios";
export class UserAPI {
getDetailed(id: number) {
return axiosClient.get(`/users/${id}`).then((response) => response.data);
}
}
```
Data/query modules use shared query helpers:
```ts
import {
createKeyBuilder,
getPaginationInfo,
} from "#infrastructure/reactQuery";
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
```
UI can consume generic app-wide formatters:
```ts
import { IntlDate } from "#infrastructure/IntlDate";
```
@@ -0,0 +1,96 @@
# Providers Layer
## Table of Contents
- [Providers Layer](#providers-layer)
- [Purpose](#purpose)
- [Responsibilities](#responsibilities)
- [Recommended Structure](#recommended-structure)
- [App Provider Composition](#app-provider-composition)
- [Example: AppProviders](#example-appproviders)
## Purpose
The `providers` layer is responsible for **global application context and environment configuration**.
It acts as the **composition root of the application**, where all global React providers are initialized and combined.
Providers make global services available across the entire application, such as:
- routing
- data fetching clients
- theme configuration
- authentication/session context
- internationalization
- notifications
- global state management
Instead of importing these providers in many places, they are **composed once and applied at the application root**.
## Responsibilities
The `providers` layer is responsible for:
- initializing global React context providers
- configuring global services
- composing multiple providers into a single entry point
- exposing the provider tree to the application root
## Recommended Structure
```plaintext
providers/
|
| --- AppProviders.tsx # main provider composition
|
| --- QueryProvider.tsx # React Query setup
| --- RouterProvider.tsx # routing configuration
| --- ThemeProvider.tsx # theme context
| --- AuthProvider.tsx # authentication/session provider
| --- I18nProvider.tsx # internationalization provider
|
| --- index.ts # re-export providers (optional)
```
## App Provider Composition
Instead of wrapping the application with multiple providers manually, they are composed in a single root provider component.
This centralizes the provider tree and simplifies application initialization.
Example usage in the application entry point:
```tsx
import { AppProviders } from "@/providers/AppProviders";
ReactDOM.createRoot(document.getElementById("root")!).render(
<AppProviders>
<App />
</AppProviders>,
);
```
### Example: AppProviders
This component acts as the global application wrapper.
```tsx
import { ReactNode, FC, PropsWithChildren } from "react";
import { QueryProvider } from "./QueryProvider";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
import { RouterProvider } from "./RouterProvider";
import { I18nProvider } from "./I18nProvider";
export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
return (
<QueryProvider>
<I18nProvider>
<AuthProvider>
<ThemeProvider>
<RouterProvider>{children}</RouterProvider>
</ThemeProvider>
</AuthProvider>
</I18nProvider>
</QueryProvider>
);
};
```
@@ -0,0 +1,109 @@
# Reference Data Layer
## Table of Contents
- [Purpose](#purpose)
- [Responsibilities](#responsibilities)
- [Recommended Structure](#recommended-structure)
- [Rules](#rules)
- [Examples](#examples)
## Purpose
The `refdata/` layer contains app-wide reference datasets that are reused across modules.
Reference data is data that usually comes from the backend, but is not owned by one feature module.
Examples:
- countries
- languages
- currencies
- time zones
- common option dictionaries
- shared lookup datasets
## Responsibilities
The `refdata/` layer is responsible for:
- fetching common reference datasets
- exposing reusable query hooks for those datasets
- defining cache keys for reference data
- keeping app-wide lookup data out of feature modules
## Recommended Structure
For small datasets, a single file per dataset is enough:
```plaintext
refdata/
├── countries.ts
├── languages.ts
└── currencies.ts
```
For larger datasets, use a folder:
```plaintext
refdata/
└── countries/
├── api.ts
├── dto.ts
├── mapper.ts
├── queries.ts
└── keys.ts
```
## Rules
1. Use `refdata/` only for app-wide datasets.
If a dataset belongs to one feature module, keep it in that module's `data/` layer.
2. `refdata/` may use infrastructure.
Reference data hooks can use `#infrastructure/axios` and `#infrastructure/reactQuery`.
3. `refdata/` should not import from `view/`, `pages/`, or feature modules.
It should remain reusable across the app.
4. Expose hooks or small service functions.
Consumers should not need to know backend endpoints or cache key details.
5. Add mappers when backend DTOs differ from app-facing reference data.
If a reference dataset performs DTO transformation, test the mapper the same way module data mappers are tested.
## Examples
```ts
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { axiosClient } from "#infrastructure/axios";
import { createKeyBuilder } from "#infrastructure/reactQuery";
export const countriesCacheKey = createKeyBuilder(
"REF_DATA",
"COUNTRIES",
"LIST",
);
interface CountryData {
code: string;
name: string;
}
export const useCountries = () => {
return useQuery({
queryKey: countriesCacheKey.store(),
placeholderData: keepPreviousData,
queryFn: async () => {
return axiosClient
.get<Array<CountryData>>("/datasets/countries")
.then((data) => data.data);
},
});
};
```
@@ -0,0 +1,252 @@
# Shared Layer
## Table of Contents
- [Purpose](#purpose)
- [Recommended Structure](#recommended-structure)
- [UI Components](#ui-components)
- [Purpose](#ui-components-purpose)
- [Core Principles](#ui-components-core-principles)
1. [Simple Components](#ui-components-core-principles-1)
2. [Composable Design](#ui-components-core-principles-2)
3. [Easily Customizable](#ui-components-core-principles-3)
4. [No Business Logic](#ui-components-core-principles-4)
- [Styling Strategy](#ui-components-styling-strategy)
- [Styling Philosophy](#ui-components-styling-strategy-philosophy)
- [Technology Stack](#ui-components-styling-strategy-tech-stack)
- [Hooks](#hooks)
- [Utilities](#utilities)
- [Libraries](#libraries)
- [Assets](#assets)
- [Styles](#styles)
## Purpose
The `shared` layer contains **global reusable building blocks used across the entire application**.
The goal is to keep these utilities **framework-level and reusable across the entire application**.
```plaintext
shared/
├── ui/ # dumb or basic UI components
│ ├── Button.tsx
│ ├── Modal.tsx
│ └── Card.tsx
│
├── widgets/ # reusable composed UI blocks
│ ├── DataGrid/
│ └── screens/
│
├── forms/ # reusable form composition helpers
│ └── composed/
│
├── editors/ # reusable editor integrations
│ ├── tinymce/
│ └── image_input/
│
├── hooks/ # reusable hooks
│ ├── useDebounce.ts
│ ├── useToggle.ts
│ └── useLocalStorage.ts
│
├── lib/ # pure helpers & utilities
│ └── formatDate.ts
│
├── assets/ # images, icons, fonts
│ ├── icons/
│ └── fonts/
│
└── styles/ # global CSS, variables, Tailwind overrides
├── globals.css
└── variables.css
```
## Recommended Structure
```plaintext
shared/
|
| --- ui/ # primitive UI components
|
| --- widgets/ # reusable composed UI blocks
|
| --- forms/ # reusable form composition
|
| --- editors/ # reusable editor wrappers
|
| --- hooks/ # generic reusable hooks
|
| --- utils/ # pure helper functions
|
| --- lib/ # pure helpers and utility services
|
| --- assets/ # images, icons, fonts
|
| --- styles/ # global styling
|
| --- constants/ # global constants
```
### UI Components
#### Purpose {#ui-components-purpose}
The goal of the UI layer is to provide simple, composable, and customizable components that can be reused across the application.
- components are simple building blocks
- components are composable
- components are fully customizable
- components are not tied to business logic
UI components should behave like Lego blocks that can be combined into larger UI structures.
#### Core Principles {#ui-components-core-principles}
##### 1. Simple Components {#ui-components-core-principles-1}
Examples:
- Button
- Input
- Card
- Modal
- Dropdown
##### 2. Composable Design {#ui-components-core-principles-2}
Components must support composition.
Composition allows developers to build complex UI without modifying base components.
Example:
```tsx
<Card>
<CardHeader />
<CardContent />
<CardFooter />
</Card>
```
```tsx
<Button>
<Icon />
Save
</Button>
```
##### 3. Easily Customizable {#ui-components-core-principles-3}
UI components should allow customization through:
- className
- variants
- props
- composition
```tsx
<Button variant="outline" size="lg">
Save
</Button>
```
##### 4. No Business Logic {#ui-components-core-principles-4}
UI components must remain presentation-only. May include local state.
They should NOT:
- call API
- use React Query
- contain form submission logic
- contain domain-specific logic
#### Styling Strategy {#ui-components-styling-strategy}
##### Styling Philosophy {#ui-components-styling-strategy-philosophy}
Styling follows these principles:
- Utility-first styling
- Design tokens for consistency
- Component-level styling
- Variants instead of style duplication
- Minimal custom CSS
##### Technology Stack {#ui-components-styling-strategy-tech-stack}
- **TailwindCSS** - primary styling system
- **class-variance-authority (CVA)** - component variants
- **CSS variables** - design tokens
- **Radix primitives** - accessible UI building blocks
Custom CSS should be minimal and centralized.
### Hooks
The `shared/hooks/` folder contains generic reusable hooks.
These hooks must not depend on entities or business logic.
```plaintext
shared/hooks/
| --- useDebounce.ts
| --- useToggle.ts
| --- usePrevious.ts
| --- useLocalStorage.ts
```
### Utilities
The `shared/utils/` folder contains pure helper functions.
These functions should be stateless and reusable
```plaintext
shared/utils/
| --- formatDate.ts
| --- formatCurrency.ts
| --- generateId.ts
| --- sleep.ts
```
### Libraries
The `shared/lib/` folder contains pure helpers and utility services.
Examples include:
- date formatting helpers
- name formatting helpers
- URL/file path helpers
- phone-number formatting helpers
```plaintext
shared/lib/
| --- time/
| --- phone_number/
| --- resolveFileURL.ts
```
### Assets
The `shared/assets/` folder stores static resources used across the application.
```plaintext
shared/assets/
| --- icons/
| --- fonts/
```
### Styles
The `shared/styles/` folder contains global styling resources.
Typical usage:
- CSS variables
- Tailwind configuration extensions
- global resets
```plaintext
shared/styles/
| --- globals.css
| --- variables.css
| --- tailwind.css
```
@@ -0,0 +1,345 @@
# 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>
```