Czy kiedykolwiek przeszukiwałeś projekt React lub Vue i natrafiłeś na dziesiątki plików typu Button.tsx, Button.vue, ButtonStyles.scss, ButtonTypes.ts, useButton.ts?
W tym artykule pokażę Ci alternatywne podejście – Folder-as-Component – które stosuję w projekcie Nucleify, modularnym frameworku full-stack. Ten wzorzec działa równie dobrze w React, Vue, Nuxt, Next.js czy dowolnej architekturze opartej na komponentach.
🎯 Problem: Chaos w nazewnictwie
Typowy projekt frontend wygląda często tak:
components/
├── Button.tsx
├── Button.vue
├── ButtonProps.ts
├── ButtonStyles.scss
├── Card.tsx
├── CardProps.ts
├── CardStyles.scss
├── Navbar.tsx
├── NavbarLinks.tsx
├── NavbarDrawer.tsx
├── NavbarStyles.scss
...
Co jest nie tak?
- Płaska struktura – wszystko w jednym folderze
-
Redundantne prefixy –
Button,Button,Button... - Brak enkapsulacji – trudno stwierdzić, co należy do czego
- Problemy ze skalowaniem – przy 50+ komponentach robi się koszmar
-
IDE tab hell – 10 otwartych tabów z
Button*w nazwie
💡 Rozwiązanie: Folder-as-Component
Zamiast nazywać pliki po komponencie, nazywam foldery po komponencie, a pliki wewnątrz zawsze mają te same nazwy:
components/
├── button/
│ ├── index.ts # eksportuje komponent + typy
│ ├── index.tsx # komponent React
│ ├── index.vue # lub komponent Vue
│ ├── _index.scss # style (underscore = SCSS partial)
│ └── types/
│ ├── index.ts # barrel export
│ └── interfaces.ts
├── card/
│ ├── index.ts
│ ├── index.tsx
│ └── _index.scss
└── navbar/
├── index.ts
├── index.tsx
├── _index.scss
└── components/ # zagnieżdżone komponenty
├── index.ts
├── _index.scss
├── Drawer/
│ ├── index.ts
│ ├── index.tsx
│ └── _index.scss
└── Links/
├── index.ts
├── index.tsx
└── _index.scss
🔥 Przykłady z życia
Zobaczmy jak wygląda struktura przycisku w React i Vue:
Przykład React: components/button/
index.ts – eksportuje komponent i typy:
export { Button } from './index.tsx'
export * from './types'
index.tsx – komponent React:
import type { ButtonProps } from '.'
import clsx from 'clsx'
import './index.scss'
export const Button = ({
variant,
size,
icon,
label,
children,
className,
...props
}: ButtonProps) => {
return (
<button
className={clsx(
'button',
variant && `${variant}-button`,
size && `${size}-button`,
className
)}
{...props}
>
{icon && <Icon name={icon} />}
{label && <span>{label}</span>}
{children}
</button>
)
}
Przykład Vue: components/button/
index.ts – eksportuje komponent i typy:
export { default as Button } from './index.vue'
export * from './types'
index.vue – komponent Vue:
<template>
<button
:class="[
'button',
variant && `${variant}-button`,
size && `${size}-button`
]"
>
<Icon v-if="icon" :name="icon" />
<span v-if="label">{{ label }}</span>
<slot />
</button>
</template>
<script setup lang="ts">
import type { ButtonProps } from '.' // Import z tego samego folderu!
const props = defineProps<ButtonProps>()
</script>
<style lang="scss">
@import 'index'; // Ładuje _index.scss
</style>
Wspólne: _index.scss
.button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: all 0.3s ease;
&.primary-button {
background: var(--primary);
color: white;
}
&.secondary-button {
background: transparent;
border: 1px solid var(--primary);
}
&.sm-button {
padding: 0.25rem 0.5rem;
}
&.md-button {
padding: 0.5rem 1rem;
}
&.lg-button {
padding: 0.75rem 1.5rem;
}
}
Wspólne: types/interfaces.ts
import type { ButtonHTMLAttributes } from 'react' // lub z vue
export type ButtonVariant = 'primary' | 'secondary' | 'ghost'
export type ButtonSize = 'sm' | 'md' | 'lg'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
label?: string
icon?: string
}
🌳 Barrel Exports – agregacja modułów
Magia dzieje się w plikach index.ts na wyższych poziomach:
components/atoms/index.ts – eksportuje wszystkie atomy:
export * from './avatar'
export * from './badge'
export * from './button'
export * from './checkbox'
export * from './heading'
export * from './icon'
export * from './image'
// ... itd.
components/sections/index.ts – eksportuje sekcje:
export * from './contact'
export * from './hero'
export * from './faq'
export * from './footer'
export * from './navbar'
Analogicznie dla SCSS:
components/sections/_index.scss:
@import 'contact', 'hero', 'faq', 'footer', 'navbar';
Dzięki temu w głównym pliku stylów wystarczy:
@import 'components/sections';
📁 Zagnieżdżone komponenty
Navbar zazwyczaj ma własne subkomponenty. Oto struktura:
navbar/
├── index.ts
├── index.tsx # lub index.vue
├── _index.scss
└── components/
├── index.ts # export * from './Drawer'; export * from './Links'
├── _index.scss # @import 'Drawer', 'Links';
├── Drawer/
│ ├── index.ts
│ ├── index.tsx
│ └── _index.scss
└── Links/
├── index.ts
├── index.tsx
├── _index.scss
└── links.ts # dane linków
navbar/index.tsx – czyste i czytelne importy:
import { NavbarDrawer, NavbarLinks } from './components'
import './index.scss'
export const Navbar = () => {
return (
<nav className="navbar">
<Logo />
<NavbarLinks />
<NavbarDrawer />
</nav>
)
}
✅ Zalety wzorca Folder-as-Component
1. Intuicyjne importy
// Zamiast:
import Button from '@/components/Button.tsx'
import { ButtonProps } from '@/components/ButtonTypes'
// Mamy:
import { Button, ButtonProps } from '@/components/button'
// lub z barrel exportu:
import { Button } from '@/components'
2. Enkapsulacja
Wszystko związane z komponentem jest w jednym miejscu. Chcesz usunąć Button? Usuwasz folder button/.
3. Skalowalność
Przy 100+ komponentach nadal masz porządek – każdy komponent to osobny "mikro-pakiet".
4. IDE-friendly
- Zakładki pokazują nazwę folderu, nie pliku
- Szybkie
Ctrl+P/Cmd+P→ wpisujeszbutton/indexi od razu wiesz co to - Mniej konfliktów nazw
5. Konsystencja
Każdy developer wie, gdzie szukać:
- Komponent →
index.tsx/index.vue - Eksporty →
index.ts - Style →
_index.scss - Typy →
types/interfaces.ts
6. Łatwe refaktorowanie
Przenosisz cały folder bez zmiany importów wewnętrznych.
7. Tree-shaking friendly
Bundler może łatwo analizować, co jest używane.
8. Niezależność od frameworka
Ta sama struktura działa w React, Vue, Svelte, Angular – zmienia się tylko rozszerzenie pliku komponentu.
⚙️ Konwencje nazewnictwa plików
index.ts vs index.tsx vs index.vue
| Plik | Cel |
|---|---|
index.ts |
Barrel exports (re-eksportuje komponent + typy) |
index.tsx |
Komponent React/Preact z JSX |
index.vue |
Vue Single File Component |
index.svelte |
Komponent Svelte |
Konwencja _index.scss
Underscore (_) w SCSS oznacza partial – plik, który nie kompiluje się samodzielnie, tylko jest importowany przez inne pliki.
// W komponencie:
@import 'index'; // Ładuje _index.scss z tego samego folderu
// W agregatorze:
@import 'button', 'card', 'navbar'; // Ładuje _index.scss z każdego folderu
🎨 Jak to wygląda w praktyce?
W prawdziwym projekcie cała architektura oparta jest na tym wzorcu:
src/
├── components/
│ ├── atoms/
│ │ ├── index.ts
│ │ ├── _index.scss
│ │ ├── button/
│ │ │ ├── index.ts
│ │ │ ├── index.tsx
│ │ │ ├── _index.scss
│ │ │ └── types/
│ │ │ ├── index.ts
│ │ │ └── interfaces.ts
│ │ ├── icon/
│ │ └── input/
│ ├── molecules/
│ │ ├── index.ts
│ │ ├── search-bar/
│ │ └── form-field/
│ └── organisms/
│ ├── index.ts
│ ├── navbar/
│ ├── footer/
│ └── sidebar/
├── hooks/
│ ├── index.ts
│ ├── use-auth.ts
│ └── use-theme.ts
└── utils/
├── index.ts
├── format-date.ts
└── cn.ts
Każdy komponent ma tę samą strukturę. Nowy developer od razu wie, gdzie co znaleźć.
🚀 Quick Start
Chcesz wdrożyć to w swoim projekcie? Oto minimalna struktura:
React
src/
└── components/
└── button/
├── index.ts # export { Button } from './index.tsx'; export * from './types'
├── index.tsx # export const Button = () => <button>...</button>
├── _index.scss
└── types/
├── index.ts
└── interfaces.ts
Vue
src/
└── components/
└── button/
├── index.ts # export { default as Button } from './index.vue'; export * from './types'
├── index.vue # <template>...</template>
├── _index.scss
└── types/
├── index.ts
└── interfaces.ts
Import
import { Button } from '@/components/button'
// lub z barrel:
import { Button } from '@/components'
📚 Podsumowanie
| Aspekt | Tradycyjne | Folder-as-Component |
|---|---|---|
| Struktura | Płaska | Hierarchiczna |
| Nazewnictwo plików | ComponentName.* |
index.* |
| Enkapsulacja | Słaba | Silna |
| Skalowalność | ❌ | ✅ |
| Importy | Długie ścieżki | Krótkie, z barrel |
| Refaktoring | Trudny | Łatwy |
| Framework | Specyficzny | Agnostyczny |
🔗 Linki
- Nucleify demo
- Nucleify na GitHubie – sprawdź pełną implementację
- Atomic Design – metodologia, która świetnie współgra z tym wzorcem
- Barrel Exports Pattern – więcej o agregacji eksportów
Masz pytania? Zostaw komentarz! 👇
A jeśli podoba Ci się ta konwencja, daj ⭐ na GitHubie Nucleify 🚀
Top comments (0)