Bạn code React 3 năm, project phình to, và bạn bắt đầu sợ mở cái folder src/components. Đó là lúc "Kiến trúc" (Architecture) lên tiếng.
Tôi đã từng maintain những dự án mà file utils.ts có 5000 dòng code, và components/Button.tsx được import ở 200 nơi với logic if-else rối rắm.
Dưới đây là đúc kết xương máu về cách tổ chức file.
1. Cái chết của Layer-based (Mô hình phân tầng)
Mô hình truyền thống (Rails style):
src/
controllers/
models/
views/Áp dụng vào React -> components/, hooks/, context/.
Tại sao nó tệ hại?
- Context Switching: Để sửa 1 tính năng "Comment", bạn phải mở 1 file hooks, 1 file component, 1 file types nằm ở 3 góc trời khác nhau. Não bạn phải 'nhảy số' liên tục.
- Coupling ngầm (The Hidden Coupling):
- Bạn thấy
src/hooks/useUser.tsvàsrc/hooks/useProduct.tsnằm cạnh nhau. Bạn tiện tay importuseProductvàouseUserđể check quyền mua hàng. - BÙM! Feature User giờ đây phụ thuộc chặt chẽ vào Feature Product. Bạn không thể tách User ra microservice riêng được nữa.
- Bạn thấy
2. Feature-based (Screaming Architecture)
Hãy nhóm theo DOMAIN (Nghiệp vụ), chứ không phải theo ROLE (Kỹ thuật).
src/
features/
auth/ <- Mọi thứ về Auth nằm ở đây
ecommerce/ <- Cart, Product, Checkout
social/ <- Profile, Feed, CommentQuy tắc "Vòng tròn đồng tâm"
Feature auth KHÔNG BAO GIỜ được biết về sự tồn tại của ecommerce.
Nhưng ecommerce CÓ THỂ chọc vào auth (để lấy user ID).
Cách giải quyết Dependency Hell:
- Core Feature: Độc lập (Auth, UI Kit).
- Composite Feature: Phụ thuộc vào Core (Checkout cần Auth + UI Kit).
3. Những cạm bẫy cần tránh (Senior Note)
❌ Trap 1: Barrel File Explosion (index.ts)
Junior thường lười: export * from './components'.
Kết quả: Import 1 cái nút, dính luôn cả cái feature. Webpack tree-shaking khóc thét.
=> Lời khuyên: Chỉ export đúng cái gì cần thiết ở features/auth/index.ts. Private component thì giấu kỹ đi.
❌ Trap 2: Cross-feature Logic (Vùng xám)
Vấn đề: Khi user thanh toán (Checkout Feature), cần gửi thông báo (Notification Feature). Code ở đâu?
- Cách 1 (Tệ): Import
sendNotificationvào file Checkout. -> Coupling. - Cách 2 (Tốt): Dùng Event Bus hoặc Global Store Actions. Checkout chỉ dispatch
PAYMENT_SUCCESS. Notification lắng nghe event đó. Không ai biết ai.
❌ Trap 3: Component chung chung "vô chủ"
Bạn tạo src/components/List.tsx.
Sau đó Auth cần List user. Product cần List hàng.
Dần dần List.tsx thành nồi lẩu thập cẩm if (type === 'user') ... else if (type === 'product').
=> Lời khuyên: Thà duplicate code một chút (AuthList, ProductList) còn hơn tạo ra một component "God Object" quá trừu tượng. DRY (Don't Repeat Yourself) đôi khi là cái bẫy. (AHA - Avoid Hasty Abstractions).
4. Folder Structure chuẩn (Ý kiến cá nhân)
src/
├── app/ # Next.js App Router (Chỉ làm nhiệm vụ Routing)
├── components/ # Base UI (Button, Input, Modal - Dumb 100%)
├── lib/ # 3rd party setup (axios, queryClient, cn)
├── features/ # NGHIỆP VỤ CHÍNH
│ ├── auth/
│ │ ├── components/ # LoginForm, RegisterModal (Smart)
│ │ ├── api/ # login(), logout()
│ │ └── index.ts # Public API
│ └── order/
├── stores/ # Global State quản lý cross-feature
└── utils/ # Helper thuần túy (date, string)Kết luận
- Đừng tối ưu hóa sớm. Nếu project < 10 files, để file nào cũng được.
- Khi project > 50 files, hãy chuyển sang Feature-based.
- Luôn tự hỏi: "Nếu tôi xóa Feature Order, tôi có cần sửa code trong Feature Auth không?". Nếu câu trả lời là CÓ -> Kiến trúc của bạn đang sai.