| name | frontend-architecture |
| description | Defines a Clean Architecture and reusable component structures for frontend. Especially useful when the user wants to organize pages, tables, lists, item cards, subpages, and modal dialogs in a predictable and maintainable way. Applies to React, Vue, Svelte, or similar frameworks. |
| license | Complete terms in LICENSE.txt |
Frontend Architecture Skill
This skill establishes a standard architecture and a component pattern for frontend, based on Clean Architecture. Its goal is to guarantee consistency, maintainability, and separation of concerns in projects of any size.
When to use this skill?
- When starting a new frontend project with professional structure
- When the user mentions "clean architecture", "layers", "use cases"
- If the project requires screens with lists, tables, cards, subpages, and modal dialogs
- To maintain a consistent pattern across pages and components
Clean Architecture in Frontend (Summary)
The architecture is organized in concentric layers, where dependencies point inward:
presentation layer (UI)
↓
use cases layer (business logic)
↓
repositories layer (abstractions)
↓
entities layer (domain models)
Recommended folder structure (React example):
src/
├── domain/ # Innermost layer: entities
│ ├── entities/ # models (e.g.: Product, User)
│ └── repositories/ # repository interfaces (e.g.: IProductRepository)
├── application/ # use cases (grouped by module when many)
│ └── useCases/
│ ├── products/
│ │ ├── GetProductsUseCase.ts
│ │ ├── CreateProductUseCase.ts
│ │ └── DeleteProductUseCase.ts
│ └── users/
│ ├── GetUsersUseCase.ts
│ └── CreateUserUseCase.ts
├── infrastructure/ # concrete repository implementations
│ └── repositories/ # (e.g.: ApiProductRepository, LocalStorageRepository)
├── presentation/ # outer layer: modules organized by feature
│ ├── products/
│ │ ├── ProductsPage.tsx
│ │ └── components/
│ │ ├── ProductsTable.tsx
│ │ ├── ProductCard.tsx
│ │ └── CreateProductDialog.tsx
│ ├── users/
│ │ ├── UsersPage.tsx
│ │ └── components/
│ │ ├── UsersTable.tsx
│ │ └── CreateUserDialog.tsx
│ ├── components/ # UI library components (e.g.: shadcn/ui)
│ │ └── ui/ # shadcn generated components (button, dialog, table, ...)
│ ├── lib/ # UI library helpers (e.g.: shadcn's utils.ts -> cn())
│ │ └── utils.ts
│ ├── shared/ # shared components across modules
│ │ └── components/
│ ├── hooks/ # hooks grouped by module when many
│ │ ├── products/
│ │ │ └── useProducts.ts
│ │ └── users/
│ │ └── useUsers.ts
│ └── contexts/ # if applicable
└── config/ # global configurations
UI library rule (shadcn, etc.): Any UI/component library that scaffolds its own
folders (such as shadcn/ui) must live inside presentation/, never at the
root of src/. This includes its components (presentation/components/ui/), its
lib/ helpers (e.g.: presentation/lib/utils.ts with the cn() function), and any
other generated assets (styles, providers, config helpers). The reason: these are
pure presentation concerns and must not leak into domain/, application/, or
infrastructure/. Configure the library so its aliases point into presentation/
(e.g.: in components.json set "aliases.components": "@/presentation/components"
and "aliases.utils": "@/presentation/lib/utils").
Rule: When a module grows in complexity (many use cases, hooks, components, repositories, etc.), all related files must be grouped in their own subfolder named after the module. This applies to any layer: useCases/products/, hooks/products/, repositories/products/, etc. Keep everything organized by module to maintain clarity at scale.
Mandatory Component Pattern
All pages and their subcomponents must follow this structure:
Standard composition hierarchy
Every screen follows this strict composition order:
Page
└─ Main component (Table / List / Cards) ← the primary content of the route
└─ Action buttons ← buttons that trigger secondary UI
├─ open Dialogs (create / edit / confirm)
└─ navigate to / mount other modules
└─ Other components (Dialogs, panels, drawers, ...)
Rules of the hierarchy:
- A Page orchestrates: it renders one main component (the table, list, or card
grid) plus the buttons that open everything else.
- Buttons never contain logic; they only open a
Dialog, navigate, or mount
another module's component.
- Everything a button opens (Dialogs, drawers, secondary modules) is a separate
component, composed by the Page, never inlined.
Reuse Dialogs — never duplicate
If a Dialog already exists for a given purpose (e.g.: a confirmation dialog, a generic
form dialog, an entity create/edit dialog), reuse it instead of creating a new one.
- Before creating a Dialog, check if an equivalent one already exists in the module's
components/ or in presentation/shared/components/.
- If the same Dialog is needed by more than one module, promote it to
presentation/shared/components/ and import it from there.
- Generic, repeated dialogs (e.g.:
ConfirmDialog, FormDialog) must be parameterized
via props (title, fields, callbacks) so a single component covers every case instead
of duplicating near-identical dialogs.
- Duplicating a Dialog that already exists is not allowed.
1. Page (main page)
A page is a component that contains the complete structure of a route. It never contains domain logic, only use case orchestration and composition of child components.
React example:
import { useProducts } from "../hooks/products/useProducts";
import { ProductsTable } from "./components/ProductsTable";
import { CreateProductDialog } from "./components/CreateProductDialog";
import { useState } from "react";
export function ProductsPage() {
const { products, loading, addProduct, deleteProduct } = useProducts();
const [isDialogOpen, setIsDialogOpen] = useState(false);
return (
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold">Products</h1>
<button
onClick={() => setIsDialogOpen(true)}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
+ Add product
</button>
</div>
{loading ? (
<div>Loading...</div>
) : (
<ProductsTable products={products} onDelete={deleteProduct} />
)}
<CreateProductDialog
open={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
onSave={addProduct}
/>
</div>
);
}
2. Tables / Lists (separate component)
Every table or list must be an independent component. It receives data and callbacks as props. It never fetches directly.
Example:
interface Product {
id: string;
name: string;
price: number;
}
interface Props {
products: Product[];
onDelete: (id: string) => void;
}
export function ProductsTable({ products, onDelete }: Props) {
return (
<table className="w-full border">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{products.map((p) => (
<tr key={p.id}>
<td>{p.name}</td>
<td>{p.price}</td>
<td>
<button onClick={() => onDelete(p.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
3. Item Card / List item
If a list with cards is used instead of a table, each item goes in its own component.
export function ProductCard({ product, onDelete }: Props) {
return (
<div className="border rounded p-4 shadow">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onDelete(product.id)}>Delete</button>
</div>
);
}
4. Subpages
If a page has a subpage (e.g.: ProductDetailPage inside the products module), it follows the same pattern: it's called Page, is composed of components (tables, cards, dialogs), and is placed inside its module folder.
Example:
export function ProductDetailPage() {
return (
<div>
<h1>Product Detail</h1>
<ProductInfo />
<RelatedProductsList />
</div>
);
}
5. Always use Dialogs (modals)
Any creation or editing form must go inside a Dialog (modal). The dialog is an independent component that receives open, onClose, onSave.
Example:
interface Props {
open: boolean;
onClose: () => void;
onSave: (product: Omit<Product, "id">) => void;
}
export function CreateProductDialog({ open, onClose, onSave }: Props) {
const [name, setName] = useState("");
const [price, setPrice] = useState(0);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({ name, price });
onClose();
setName("");
setPrice(0);
};
if (!open) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg w-96">
<h2>Create Product</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full border p-2 mb-2"
/>
<input
type="number"
placeholder="Price"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
className="w-full border p-2 mb-4"
/>
<div className="flex justify-end gap-2">
<button type="button" onClick={onClose}>
Cancel
</button>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Save
</button>
</div>
</form>
</div>
</div>
);
}
Hook to orchestrate use cases (optional but recommended)
Hooks act as a bridge between the UI and use cases. They contain state and call use cases.
import { useEffect, useState } from "react";
import { GetProductsUseCase } from "../../../application/useCases/products/GetProductsUseCase";
import { CreateProductUseCase } from "../../../application/useCases/products/CreateProductUseCase";
import { DeleteProductUseCase } from "../../../application/useCases/products/DeleteProductUseCase";
import { Product } from "../../domain/entities/Product";
export function useProducts() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const getProductsUseCase = new GetProductsUseCase();
const createProductUseCase = new CreateProductUseCase();
const deleteProductUseCase = new DeleteProductUseCase();
useEffect(() => {
getProductsUseCase
.execute()
.then(setProducts)
.finally(() => setLoading(false));
}, []);
const addProduct = async (product: Omit<Product, "id">) => {
const newProduct = await createProductUseCase.execute(product);
setProducts((prev) => [...prev, newProduct]);
};
const deleteProduct = async (id: string) => {
await deleteProductUseCase.execute(id);
setProducts((prev) => prev.filter((p) => p.id !== id));
};
return { products, loading, addProduct, deleteProduct };
}
Non-negotiable Rules
- Pages NEVER contain domain logic or direct fetch.
- Tables/lists are separate components, they receive data via props.
- Item cards are also separate components.
- Subpages follow exactly the same pattern as main pages.
- Every creation/editing form must go inside a
Dialog.
- Use cases are classes or pure functions that don't depend on the UI.
- Repositories are injected into use cases (dependency inversion).
- Each module has its own folder with its page and a
components/ subfolder.
- When a module grows, all its related files (use cases, hooks, repositories, components, etc.) must be grouped in subfolders named after the module across all layers.
- UI libraries (shadcn/ui, etc.) and everything they generate — components (
components/ui/), their lib/ helpers (utils.ts / cn()), styles and config helpers — always live inside presentation/, never at the root of src/ nor in domain/, application/, or infrastructure/.
- Composition hierarchy is fixed:
Page → main component (Table/List/Cards) + buttons → the Dialogs / modules those buttons open. Buttons only open Dialogs, navigate, or mount other modules; they hold no logic.
- Never duplicate a Dialog: if an equivalent Dialog already exists, reuse it. Shared dialogs go in
presentation/shared/components/ and generic ones (ConfirmDialog, FormDialog) are parameterized via props.
Quick Template for New Pages
When the user asks to create a new page, automatically generate:
presentation/{module}/{Module}Page.tsx (with layout, title, add button, and hook usage)
presentation/{module}/components/{Module}Table.tsx or {Module}List.tsx
presentation/{module}/components/{Module}Dialog.tsx (for create/edit)
presentation/hooks/use{Module}.ts (with corresponding use cases)