Component Organization

Component architecture patterns, organization strategies, and reusable component design principles.

Component Organization

This section covers how React components are organized, structured, and designed for maximum reusability and maintainability throughout the application.

Component Architecture Philosophy

1. Atomic Design Principles

Components are organized following atomic design methodology:

Atoms → Molecules → Organisms → Templates → Pages

Atoms (Basic Building Blocks)

  • Button, Input, Label, Icon, Badge
  • No dependencies on other components
  • Highly reusable across the application

Molecules (Simple Groups)

  • Form fields (Label + Input + Error)
  • Navigation items, Search boxes
  • Combine atoms to create simple functionality

Organisms (Complex Components)

  • Data tables, Forms, Navigation bars
  • Combine molecules and atoms
  • Business logic and state management

Templates (Page Layouts)

  • Page structures without content
  • Define content areas and layouts
  • Reusable across similar pages

Pages (Complete Interfaces)

  • Specific implementations of templates
  • Real content and data
  • Route-specific functionality

2. Component Directory Structure

components/
├── ui/                          # Atomic components (atoms)
│   ├── button/
│   │   ├── index.tsx
│   │   ├── button.stories.tsx
│   │   ├── button.test.tsx
│   │   └── button.module.css
│   ├── input/
│   │   ├── index.tsx
│   │   ├── input.stories.tsx
│   │   ├── input.test.tsx
│   │   └── input.module.css
│   ├── badge/
│   ├── icon/
│   ├── spinner/
│   └── index.ts                 # Barrel export
├── forms/                       # Molecules & form components
│   ├── form-field/
│   │   ├── index.tsx
│   │   ├── form-field.test.tsx
│   │   └── form-field.stories.tsx
│   ├── search-box/
│   ├── date-picker/
│   ├── file-uploader/
│   └── index.ts
├── navigation/                  # Navigation-specific components
│   ├── navbar/
│   ├── sidebar/
│   ├── breadcrumb/
│   ├── pagination/
│   └── index.ts
├── data-display/                # Data presentation components
│   ├── data-table/
│   │   ├── index.tsx
│   │   ├── data-table.test.tsx
│   │   ├── components/
│   │   │   ├── table-header.tsx
│   │   │   ├── table-row.tsx
│   │   │   ├── table-cell.tsx
│   │   │   ├── table-pagination.tsx
│   │   │   └── table-filters.tsx
│   │   └── hooks/
│   │       ├── use-table-state.ts
│   │       └── use-table-data.ts
│   ├── chart/
│   ├── card/
│   ├── stat-display/
│   └── index.ts
├── feedback/                    # User feedback components
│   ├── toast/
│   ├── modal/
│   ├── alert/
│   ├── confirmation-dialog/
│   └── index.ts
├── layouts/                     # Layout components (templates)
│   ├── app-layout/
│   │   ├── index.tsx
│   │   ├── components/
│   │   │   ├── header.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── main-content.tsx
│   │   │   └── footer.tsx
│   │   └── app-layout.test.tsx
│   ├── auth-layout/
│   ├── marketing-layout/
│   └── index.ts
├── features/                    # Feature-specific organisms
│   ├── product/
│   │   ├── product-form/
│   │   │   ├── index.tsx
│   │   │   ├── components/
│   │   │   │   ├── basic-info.tsx
│   │   │   │   ├── pricing.tsx
│   │   │   │   ├── inventory.tsx
│   │   │   │   └── images.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── use-product-form.ts
│   │   │   │   └── use-image-upload.ts
│   │   │   └── product-form.test.tsx
│   │   ├── product-card/
│   │   ├── product-list/
│   │   ├── product-search/
│   │   └── index.ts
│   ├── inventory/
│   │   ├── inventory-table/
│   │   ├── stock-adjustment/
│   │   ├── movement-history/
│   │   └── index.ts
│   ├── orders/
│   │   ├── order-form/
│   │   ├── order-table/
│   │   ├── order-status/
│   │   └── index.ts
│   └── analytics/
│       ├── dashboard-stats/
│       ├── performance-chart/
│       └── index.ts
└── providers/                   # Context providers
    ├── theme-provider/
    ├── auth-provider/
    ├── data-provider/
    └── index.ts

Component Design Patterns

1. Compound Components

// components/ui/accordion/index.tsx
interface AccordionContextType {
  activeItem: string | null
  setActiveItem: (item: string | null) => void
}

const AccordionContext = createContext<AccordionContextType | null>(null)

export function Accordion({ children, defaultOpen }: AccordionProps) {
  const [activeItem, setActiveItem] = useState<string | null>(defaultOpen || null)
  
  return (
    <AccordionContext.Provider value={{ activeItem, setActiveItem }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  )
}

export function AccordionItem({ children, value }: AccordionItemProps) {
  return (
    <div className="accordion-item" data-value={value}>
      {children}
    </div>
  )
}

export function AccordionTrigger({ children, value }: AccordionTriggerProps) {
  const { activeItem, setActiveItem } = useAccordionContext()
  const isActive = activeItem === value
  
  return (
    <button
      className={cn("accordion-trigger", isActive && "active")}
      onClick={() => setActiveItem(isActive ? null : value)}
    >
      {children}
      <ChevronDownIcon className={cn("transition-transform", isActive && "rotate-180")} />
    </button>
  )
}

export function AccordionContent({ children, value }: AccordionContentProps) {
  const { activeItem } = useAccordionContext()
  const isActive = activeItem === value
  
  return (
    <div className={cn("accordion-content", !isActive && "hidden")}>
      {children}
    </div>
  )
}

// Usage
<Accordion defaultOpen="item-1">
  <AccordionItem value="item-1">
    <AccordionTrigger value="item-1">What is Smart Shelf?</AccordionTrigger>
    <AccordionContent value="item-1">
      Smart Shelf is an inventory management system...
    </AccordionContent>
  </AccordionItem>
</Accordion>

2. Render Props Pattern

// components/data-display/data-fetcher/index.tsx
interface DataFetcherProps<T> {
  url: string
  children: (data: {
    data: T | null
    loading: boolean
    error: Error | null
    refetch: () => void
  }) => React.ReactNode
}

export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)
  
  const fetchData = useCallback(async () => {
    try {
      setLoading(true)
      setError(null)
      const response = await fetch(url)
      const result = await response.json()
      setData(result)
    } catch (err) {
      setError(err as Error)
    } finally {
      setLoading(false)
    }
  }, [url])
  
  useEffect(() => {
    fetchData()
  }, [fetchData])
  
  return (
    <>
      {children({ data, loading, error, refetch: fetchData })}
    </>
  )
}

// Usage
<DataFetcher<Product[]> url="/api/products">
  {({ data, loading, error, refetch }) => {
    if (loading) return <Spinner />
    if (error) return <ErrorMessage error={error} onRetry={refetch} />
    if (!data) return <EmptyState />
    
    return <ProductList products={data} />
  }}
</DataFetcher>

3. Higher-Order Components (HOCs)

// components/hoc/with-auth.tsx
export function withAuth<P extends object>(
  Component: React.ComponentType<P>,
  requiredRole?: UserRole
) {
  return function AuthenticatedComponent(props: P) {
    const { user, loading } = useAuth()
    
    if (loading) {
      return <Spinner />
    }
    
    if (!user) {
      redirect('/login')
      return null
    }
    
    if (requiredRole && !hasRole(user, requiredRole)) {
      return <UnauthorizedMessage />
    }
    
    return <Component {...props} />
  }
}

// Usage
const AdminPanel = withAuth(AdminPanelComponent, UserRole.ADMIN)

4. Custom Hooks for State Logic

// components/features/product/hooks/use-product-form.ts
interface UseProductFormProps {
  productId?: string
  onSuccess?: (product: Product) => void
  onError?: (error: Error) => void
}

export function useProductForm({ productId, onSuccess, onError }: UseProductFormProps) {
  const [loading, setLoading] = useState(false)
  const [errors, setErrors] = useState<Record<string, string>>({})
  
  const form = useForm<ProductFormData>({
    resolver: zodResolver(productSchema),
    defaultValues: {
      name: '',
      sku: '',
      price: 0,
      category_id: ''
    }
  })
  
  // Load existing product data
  useEffect(() => {
    if (productId) {
      loadProduct(productId)
    }
  }, [productId])
  
  const loadProduct = async (id: string) => {
    try {
      setLoading(true)
      const product = await productService.getById(id)
      if (product) {
        form.reset(product)
      }
    } catch (error) {
      onError?.(error as Error)
    } finally {
      setLoading(false)
    }
  }
  
  const handleSubmit = async (data: ProductFormData) => {
    try {
      setLoading(true)
      setErrors({})
      
      const product = productId
        ? await productService.update(productId, data)
        : await productService.create(data)
      
      onSuccess?.(product)
      
      if (!productId) {
        form.reset()
      }
    } catch (error) {
      if (error instanceof ValidationError) {
        setErrors(error.fieldErrors)
      } else {
        onError?.(error as Error)
      }
    } finally {
      setLoading(false)
    }
  }
  
  return {
    form,
    loading,
    errors,
    handleSubmit: form.handleSubmit(handleSubmit),
    reset: form.reset
  }
}

// Usage in component
export function ProductForm({ productId, onSuccess }: ProductFormProps) {
  const { form, loading, errors, handleSubmit } = useProductForm({
    productId,
    onSuccess
  })
  
  return (
    <form onSubmit={handleSubmit}>
      <FormField
        label="Product Name"
        error={errors.name}
        {...form.register('name')}
      />
      {/* Other form fields */}
      <Button type="submit" loading={loading}>
        {productId ? 'Update' : 'Create'} Product
      </Button>
    </form>
  )
}

Component Documentation

1. Component Props Interface

// components/ui/button/index.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** Button variant style */
  variant?: 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link'
  /** Button size */
  size?: 'sm' | 'md' | 'lg'
  /** Loading state */
  loading?: boolean
  /** Icon to display before text */
  icon?: React.ReactNode
  /** Full width button */
  fullWidth?: boolean
  /** Button content */
  children: React.ReactNode
}

/**
 * Reusable button component with multiple variants and states
 * 
 * @example
 * ```tsx
 * <Button variant="primary" size="md" loading={isSubmitting}>
 *   Save Product
 * </Button>
 * ```
 */
export function Button({
  variant = 'primary',
  size = 'md',
  loading = false,
  icon,
  fullWidth = false,
  children,
  className,
  disabled,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        buttonVariants({ variant, size }),
        fullWidth && 'w-full',
        className
      )}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <Spinner size="sm" />}
      {!loading && icon && <span className="icon">{icon}</span>}
      {children}
    </button>
  )
}

2. Storybook Stories

// components/ui/button/button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './index'
import { PlusIcon } from '@heroicons/react/24/outline'

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  parameters: {
    layout: 'centered'
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'destructive', 'ghost', 'link']
    },
    size: {
      control: { type: 'select' },
      options: ['sm', 'md', 'lg']
    }
  }
}

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Button'
  }
}

export const WithIcon: Story = {
  args: {
    variant: 'primary',
    icon: <PlusIcon className="w-4 h-4" />,
    children: 'Add Product'
  }
}

export const Loading: Story = {
  args: {
    variant: 'primary',
    loading: true,
    children: 'Saving...'
  }
}

export const AllVariants: Story = {
  render: () => (
    <div className="flex gap-2">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  )
}

3. Component Tests

// components/ui/button/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './index'

describe('Button', () => {
  it('renders with default props', () => {
    render(<Button>Click me</Button>)
    const button = screen.getByRole('button', { name: 'Click me' })
    expect(button).toBeInTheDocument()
    expect(button).toHaveClass('btn-primary', 'btn-md')
  })
  
  it('handles click events', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
  
  it('shows loading state', () => {
    render(<Button loading>Loading</Button>)
    const button = screen.getByRole('button')
    expect(button).toBeDisabled()
    expect(screen.getByTestId('spinner')).toBeInTheDocument()
  })
  
  it('renders with icon', () => {
    const icon = <span data-testid="test-icon">🎯</span>
    render(<Button icon={icon}>With Icon</Button>)
    
    expect(screen.getByTestId('test-icon')).toBeInTheDocument()
    expect(screen.getByText('With Icon')).toBeInTheDocument()
  })
  
  it('applies variant classes correctly', () => {
    const { rerender } = render(<Button variant="secondary">Button</Button>)
    expect(screen.getByRole('button')).toHaveClass('btn-secondary')
    
    rerender(<Button variant="destructive">Button</Button>)
    expect(screen.getByRole('button')).toHaveClass('btn-destructive')
  })
})

Component Best Practices

1. Prop Design

  • Use TypeScript interfaces for props
  • Provide sensible defaults
  • Keep props minimal and focused
  • Use composition over configuration

2. State Management

  • Keep component state local when possible
  • Use custom hooks for complex state logic
  • Lift state up when needed by multiple components
  • Consider context for deeply nested state

3. Performance

  • Use React.memo for expensive components
  • Implement useMemo and useCallback appropriately
  • Avoid inline objects and functions in JSX
  • Consider component lazy loading

4. Accessibility

  • Include proper ARIA attributes
  • Ensure keyboard navigation works
  • Provide screen reader friendly content
  • Test with accessibility tools

5. Testing

  • Test component behavior, not implementation
  • Use appropriate testing utilities
  • Mock external dependencies
  • Test error states and edge cases

This component organization strategy provides a scalable, maintainable structure for building complex React applications with clear separation of concerns and reusable components.