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.