Rules for component size, naming conventions, file structure, TypeScript strictness, and no magic strings. Apply when creating any component, naming files, or reviewing code.
CRITICAL: A single component file must never exceed 300–350 lines of code.
If a component is growing beyond this limit, apply one or more of these strategies:
| Strategy | How |
|---|---|
| Extract sub-components | Move sections into separate *.tsx files in the same folder |
| Move logic to a hook | Extract useState, useEffect, handlers → useYourFeature.ts in shared/lib/hooks/ |
| Split large forms | Each field group becomes its own component file |
| Extract utilities | Pure helper functions → shared/lib/utils/ |
// ❌ One giant file
src/shared/components/forms/checkoutForm/checkoutForm.tsx (600 lines)
// ✅ Split into focused pieces
src/shared/components/forms/checkoutForm/
├── checkoutForm.tsx (70 lines — orchestrator only)
├── shippingFields.tsx (80 lines)
├── paymentFields.tsx (90 lines)
├── orderSummary.tsx (60 lines)
└── useCheckoutForm.ts (80 lines — all logic + state)
CRITICAL: All component folders and filenames must use camelCase.
// ✅ Correct — camelCase
src/shared/components/ui/button/button.tsx
src/shared/components/ui/themeToggle/themeToggle.tsx
src/shared/components/ui/fileUpload/fileUpload.tsx
src/shared/components/forms/loginForm/loginForm.tsx
src/shared/components/forms/loginForm/emailField.tsx
// ❌ Wrong — PascalCase folders/files
src/shared/components/ui/Button/Button.tsx
src/shared/components/forms/LoginForm/LoginForm.tsx
// ❌ Wrong — kebab-case
src/shared/components/ui/theme-toggle/theme-toggle.tsx
src/shared/components/forms/login-form/login-form.tsx
The React component exported from the file is still PascalCase (e.g.,
export const ThemeToggle). Only the file and folder names use camelCase.
Follow this order within every component file:
// 1. External imports
import { useState } from 'react';
// 2. Internal imports (styles last)
import { ROUTES } from '@/shared/lib/config/routes';
import { images } from '@/shared/assets/images';
import styles from './componentName.module.scss';
// 3. TypeScript interface
interface ComponentNameProps {
label: string;
variant?: 'primary' | 'secondary';
onChange?: (value: string) => void;
}
// 4. Component
export const ComponentName = ({ label, variant = 'primary', onChange }: ComponentNameProps) => {
// State
const [value, setValue] = useState('');
// Handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
onChange?.(e.target.value);
};
// Render
return (
<div className={[styles.component, styles[`component--${variant}`]].filter(Boolean).join(' ')}>
...
</div>
);
};
Use filter(Boolean).join(' ') — never template literals with ternaries for multiple classes:
// ✅ Clean
className={[
styles.button,
styles[`button--${variant}`],
disabled ? styles['button--disabled'] : '',
className,
].filter(Boolean).join(' ')}
// ❌ Messy
className={`${styles.button} ${styles[`button--${variant}`]} ${disabled ? styles['button--disabled'] : ''}`}
interface over type for component propsas const for enum-like objectsany — use unknown and narrow the type insteadIf a function is growing long due to if/else or conditional logic, split it into separate focused functions rather than one long branching function.
// ❌ One long function with branches
const handleSubmit = async (type: 'create' | 'update') => {
if (type === 'create') {
// 20 lines of create logic
} else {
// 20 lines of update logic
}
};
// ✅ Two focused functions
const handleCreate = async () => {
// create logic only
};
const handleUpdate = async () => {
// update logic only
};
ROUTES.* (see .agent/skills/routes/SKILL.md)images.* / icons.* (see .agent/rules/assets/SKILL.md)