DEV Community

Nucleify
Nucleify

Posted on

Dlaczego warto używać plików index: Wzorzec Folder-as-Component

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
...
Enter fullscreen mode Exit fullscreen mode

Co jest nie tak?

  1. Płaska struktura – wszystko w jednym folderze
  2. Redundantne prefixyButton, Button, Button...
  3. Brak enkapsulacji – trudno stwierdzić, co należy do czego
  4. Problemy ze skalowaniem – przy 50+ komponentach robi się koszmar
  5. 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
Enter fullscreen mode Exit fullscreen mode

🔥 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'
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Przykład Vue: components/button/

index.ts – eksportuje komponent i typy:

export { default as Button } from './index.vue'
export * from './types'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

🌳 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.
Enter fullscreen mode Exit fullscreen mode

components/sections/index.ts – eksportuje sekcje:

export * from './contact'
export * from './hero'
export * from './faq'
export * from './footer'
export * from './navbar'
Enter fullscreen mode Exit fullscreen mode

Analogicznie dla SCSS:

components/sections/_index.scss:

@import 'contact', 'hero', 'faq', 'footer', 'navbar';
Enter fullscreen mode Exit fullscreen mode

Dzięki temu w głównym pliku stylów wystarczy:

@import 'components/sections';
Enter fullscreen mode Exit fullscreen mode

📁 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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

✅ 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'
Enter fullscreen mode Exit fullscreen mode

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 → wpisujesz button/index i 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
Enter fullscreen mode Exit fullscreen mode

🎨 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Import

import { Button } from '@/components/button'
// lub z barrel:
import { Button } from '@/components'
Enter fullscreen mode Exit fullscreen mode

📚 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


Masz pytania? Zostaw komentarz! 👇

A jeśli podoba Ci się ta konwencja, daj ⭐ na GitHubie Nucleify 🚀

Top comments (0)