9.9 KiB
Frontend guidelines (React + TypeScript)
This document contains coding standards for the Databasus frontend (React 19 + TypeScript + Vite + Ant Design + TailwindCSS).
For project-wide engineering philosophy, see the root CLAUDE.md.
Table of Contents
- UI kit and icons
- React component structure
- Vertical spacing
- Clipboard operations
- Forms
- User-facing copy
- FSD (Feature-Sliced Design)
- Refactoring
UI kit and icons
- AntD 5 only for components. Don't pull in Mantine, MUI, Chakra, shadcn, or Radix directly. Use AntD primitives (
Button,Input,Modal,Form,Table,Menu,Tabs, etc.) plus Tailwind utility classes for layout and spacing. @ant-design/iconsonly for icons. Don't addlucide-react,@heroicons/react,react-icons, or FontAwesome.
React component structure
Write React components with the following structure:
interface Props {
someValue: SomeValue;
}
const someHelperFunction = () => {
...
}
export const ReactComponent = ({ someValue }: Props): JSX.Element => {
// First put states
const [someState, setSomeState] = useState<...>(...)
// Then place functions
const loadSomeData = async () => {
...
}
// Then hooks
useEffect(() => {
loadSomeData();
});
// Then calculated values
const calculatedValue = someValue.calculate();
return <div> ... </div>
}
Structure order
- Props interface — Define component props
- Helper functions (outside component) — Pure utility functions
- Component declaration
- States —
useStatedeclarations - Plain functions — Event handlers, async operations, in-component formatters. Anything that does not call a React hook.
- Hooks —
useRef+ ref mutation,useCallback,useMemo,useEffect. A function wrapped inuseCallbackis a hook — it lives here, not in the functions section. - Calculated values — Derived data computed inline (e.g. an AntD
columnsarray). - Return — JSX markup
- States —
All hooks (including every useEffect) come below every plain function definition. If a useEffect reads a handler, the handler must be defined above it — don't reorder by putting handlers below the effects.
Vertical spacing
- Structure function bodies with vertical rhythm. Put a blank line between logically distinct steps (setup / main work / return, independent branches, before and after a guard). Do not leave blank lines at the start or end of a body, never stack two in a row. Do not insert blanks inside a tight expression, a single statement split across lines, or a short (≤ 5-line) function. If blank lines alone aren't enough to navigate a body, extract — don't add comments.
Clipboard operations
Always use ClipboardHelper (shared/lib/ClipboardHelper.ts) for clipboard operations — never call navigator.clipboard directly.
- Copy:
ClipboardHelper.copyToClipboard(text)— usesnavigator.clipboardwithexecCommand('copy')fallback for non-secure contexts (HTTP). - Paste: Check
ClipboardHelper.isClipboardApiAvailable()first. If available, useClipboardHelper.readFromClipboard(). If not, showClipboardPasteModalComponent(shared/ui) which lets the user paste manually via a text input modal.
Forms
Progressive disclosure
- Submit buttons (
Save,Update password, etc.) render only when the form is dirty. - Dependent fields (e.g.
Confirm password) render only after the field they depend on has a value.
User-facing copy
Use a plain hyphen - in any string the user will see — labels, descriptions, notifications, modal bodies, error messages. Reserve em dashes (—) and en dashes (–) for markdown docs and code comments only.
FSD (Feature-Sliced Design)
The project follows FSD v2.1. Layers in use: app/, pages/, widgets/, features/, entities/, shared/.
Import direction
app → pages → widgets → features → entities → shared
A module may only import from layers strictly below it. Cross-imports between slices on the same layer are forbidden.
// ✅ Allowed
import { useUser } from '@/entities/user';
import { LoginForm } from '@/features/auth';
// ❌ Violations
import { loginUser } from '@/features/auth';
// inside entities/
import { likePost } from '@/features/like-post';
// inside another feature
import { ProfilePage } from '@/pages/profile';
import { Button } from '@/shared/ui/Button';
// inside a feature
Where new code goes
- Used in only one page → keep it in that
pages/slice. - Reusable infrastructure, no business logic →
shared/(UI kit, utils, API client, route constants, auth tokens, CRUD helpers). - User interaction reused in 2+ pages →
features/. - Domain model reused in 2+ pages/features →
entities/. - App-wide providers, router, theme →
app/.
When in doubt — keep it in pages/. Extract only when a second real consumer appears.
Quick placement table
| Scenario | Single use | Reused in 2+ places |
|---|---|---|
| Profile form | pages/profile/ui/ProfileForm.tsx |
features/profile-form/ |
| Database card | pages/databases/ui/DatabaseCard.tsx |
entities/database/ui/DatabaseCard.tsx |
| Data fetching for backup | pages/backup/api/fetch-backup.ts |
entities/backup/api/ |
| Auth token / session | shared/auth/ (always) |
shared/auth/ (always) |
| Login form | pages/login/ui/LoginForm.tsx |
features/auth/ |
| CRUD helpers | shared/api/ (always) |
shared/api/ (always) |
| Date formatting util | — | shared/lib/format-date.ts |
| Modal content | pages/[page]/ui/SomeModal.tsx |
— |
MUST rules
- Downward-only imports. No upward imports, no same-layer cross-imports.
- Public API via
index.ts. External consumers import only from a slice'sindex.ts, never its internal files.// ✅ import { LoginForm } from '@/features/auth' // ❌ import { LoginForm } from '@/features/auth/ui/LoginForm' - Domain-based file names. Name files by the domain they represent, not their technical role.
// ❌ model/types.ts, model/utils.ts, lib/helpers.ts // ✅ model/user.ts, model/backup.ts, api/fetch-database.ts - No business logic in
shared/. Shared holds only infrastructure. Domain calculations live inentities/or higher. - One type per file in
model// DTOs. Eachinterface,class, orenumthat represents a domain entity, DTO, request body, or response shape gets its own file inmodel/(ormodels/), named after the type. Don't co-locate sibling types likeFoo+FooStatus+FooResponsein a singleFoo.ts— split them. Re-export each from the sliceindex.tsso consumers still import from the slice's public API.Tiny shape-only helper types tightly coupled to a parent type (a literal-union alias, a// ❌ model/RestoreVerification.ts — enums + table-stat + main interface all in one file // ✅ model/RestoreVerification.ts — interface RestoreVerification // ✅ model/RestoreVerificationTableStat.ts — interface RestoreVerificationTableStat // ✅ model/VerificationStatus.ts — enum VerificationStatus // ✅ model/VerificationTrigger.ts — enum VerificationTriggerPick<>) may stay in the parent's file. Splitting applies to anything with its own identity — anything you'd reasonably import on its own elsewhere.
Segments inside a slice
ui/— components, stylesmodel/— state, types, domain logic, validationapi/— backend calls, request functions, API-specific typeslib/— internal helpers for this slice onlyconfig/— slice-level config / feature flags
app/ and shared/ have segments only, no slices. Segments within them may import from each other.
AVOID
- Creating an entity prematurely (single consumer → keep it in the page).
- Putting CRUD inside
entities/— CRUD is infrastructure, goes toshared/api/. - Creating a
userentity just for auth tokens/DTOs — those belong inshared/auth/orshared/api/. - Extracting single-use code "for future reuse."
- God slices (
user-management/covering auth + profile + password) — split by focused responsibility. - Importing UI segments of one entity from another entity. Entity UI may be imported only from features/widgets/pages.
- Abusing the
@xcross-import pattern — it is a last resort, not a tool.
Cross-imports between same-layer slices
When two slices on the same layer need to share code, try in order:
- Merge — if they always change together, they are one slice.
- Extract shared logic down to
entities/— keep UI in the features/widgets. - Compose in a higher layer (IoC) — the parent page/widget imports both and wires them together via props/slots.
@xnotation — explicit, documented cross-import between entities only. Last resort.
Refactoring
When applying changes, do not forget to refactor old code. You can shortify, make more readable, improve code quality, etc. Common logic can be extracted to functions, constants, files, etc.
After each large change with more than ~50-100 lines of code:
- Run
pnpm format(fromfrontend/root folder) - Run
pnpm lintto verify the change