Skip to main content

Overview

Dashboard applications require careful organization of data-heavy features, real-time updates, and complex UI components. This example demonstrates how to structure an admin dashboard following the Scope Rule.

Project Structure

src/
  app/
    (dashboard)/                   # Dashboard feature group
      dashboard/
        page.tsx                   # /dashboard route
        loading.tsx                # Loading state
        error.tsx                  # Error boundary
        _components/               # Dashboard-specific
          stats-card.tsx
          dashboard-grid.tsx
          recent-activity.tsx
          quick-actions.tsx

      analytics/
        page.tsx                   # /analytics route
        _components/               # Analytics-specific
          chart-container.tsx
          metrics-table.tsx
          date-range-picker.tsx
          export-button.tsx

      users/
        page.tsx                   # /users listing
        [id]/
          page.tsx                 # /users/[id] detail
        _components/               # Users-specific
          user-table.tsx
          user-form.tsx
          user-avatar.tsx
          role-selector.tsx

      settings/
        page.tsx                   # /settings route
        profile/
          page.tsx                 # /settings/profile
        security/
          page.tsx                 # /settings/security
        _components/               # Settings-specific
          settings-form.tsx
          password-change.tsx
          notification-prefs.tsx

      reports/
        page.tsx                   # /reports listing
        [id]/
          page.tsx                 # /reports/[id]
        _components/               # Reports-specific
          report-builder.tsx
          report-preview.tsx
          filter-panel.tsx

      _components/                 # Shared dashboard components
        sidebar-nav.tsx          # Used across all dashboard pages
        breadcrumbs.tsx          # Used across all dashboard pages
        page-header.tsx          # Used across all dashboard pages
      _hooks/
        use-dashboard.ts
        use-realtime.ts
      _actions/
        dashboard-actions.ts
      layout.tsx                   # Dashboard layout

    (auth)/
      login/
        page.tsx
      layout.tsx

  shared/                          # ONLY for 2+ route groups
    components/
      ui/
        button.tsx
        card.tsx
        input.tsx
        select.tsx
        modal.tsx
        table.tsx
      charts/                      # Used in analytics & reports
        line-chart.tsx
        bar-chart.tsx
        pie-chart.tsx
      data-table/                  # Used in users & reports
        data-table.tsx
        column-header.tsx
        pagination.tsx
    hooks/
      use-debounce.ts
      use-local-storage.ts
      use-media-query.ts

  lib/
    auth.ts
    db.ts
    websocket.ts
    utils.ts
    chart-utils.ts

Key Architectural Decisions

Usage: Dashboard, Analytics, Users, Settings, Reports (5+ pages) Decision: Shared within dashboard feature group Reasoning: Used by all dashboard pages. In Next.js, it goes in (dashboard)/_components/. In Angular/React, it goes in shared/components/.

Chart Components

Usage: Analytics feature, Reports feature (2 features) Decision: Shared components Reasoning: Crosses feature boundaries. Both analytics and reports need visualization components.

User Avatar

Usage: Users feature only Decision: Local to users feature Reasoning: Specific to user management. Even if it appears in multiple places within user management, it stays local.

Data Table

Usage: Users feature, Reports feature (2 features) Decision: Shared component Reasoning: Generic table component used across multiple features. Perfect candidate for shared.

Stats Card

Usage: Dashboard page only Decision: Local to dashboard feature Reasoning: Specific to the main dashboard view. Doesn’t need to be shared.

Real-time Updates Pattern

Dashboards often need real-time data. Here’s how to structure WebSocket integration:
// lib/websocket.ts (shared infrastructure)
import { useEffect, useState } from 'react';

export function useWebSocket(url: string) {
  const [data, setData] = useState(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => setIsConnected(true);
    ws.onmessage = (event) => setData(JSON.parse(event.data));
    ws.onclose = () => setIsConnected(false);

    return () => ws.close();
  }, [url]);

  return { data, isConnected };
}
// features/dashboard/hooks/use-dashboard-data.ts (feature-specific)
import { useWebSocket } from '@/lib/websocket';

export function useDashboardData() {
  const { data, isConnected } = useWebSocket('/api/dashboard/stream');
  
  return {
    stats: data?.stats,
    recentActivity: data?.activity,
    isConnected,
  };
}

State Management Patterns

Server State

Use Server Components (Next.js) or signals (Angular) for data fetching and server state management.

Client State

Keep UI state local to components. Use context or stores only for truly global state.

Real-time State

WebSocket connections managed at the feature level with shared infrastructure.

Form State

Form state stays local to the form component. Use controlled components or form libraries.

Data Visualization Structure

Chart components are used by multiple features (analytics and reports), so they’re shared:
// shared/components/charts/line-chart.tsx
import { Line } from 'react-chartjs-2';

interface LineChartProps {
  data: ChartData;
  options?: ChartOptions;
}

export function LineChart({ data, options }: LineChartProps) {
  return <Line data={data} options={options} />;
}
But chart configurations are feature-specific:
// features/analytics/components/chart-container.tsx
import { LineChart } from '@/shared/components/charts/line-chart';
import { useAnalyticsData } from '../hooks/use-analytics-data';

export function ChartContainer() {
  const { chartData } = useAnalyticsData();
  
  // Analytics-specific chart configuration
  const options = {
    responsive: true,
    plugins: {
      legend: { display: true },
      tooltip: { mode: 'index' },
    },
  };
  
  return <LineChart data={chartData} options={options} />;
}

Layout Composition

Dashboard applications typically have nested layouts:
// app/(dashboard)/layout.tsx (Next.js)
import { SidebarNav } from './_components/sidebar-nav';
import { Breadcrumbs } from './_components/breadcrumbs';

export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard-layout">
      <SidebarNav />
      <main>
        <Breadcrumbs />
        {children}
      </main>
    </div>
  );
}
These layout components (SidebarNav, Breadcrumbs) are shared within the dashboard feature group because they’re used by all dashboard pages.

Permission-Based UI

Role-based access control stays in core/infrastructure:
// lib/auth.ts
export function hasPermission(user: User, permission: string): boolean {
  return user.permissions.includes(permission);
}

export function requireRole(requiredRole: string) {
  // Middleware or guard logic
}
Used in features:
// features/users/components/user-form.tsx
import { hasPermission } from '@/lib/auth';
import { useUser } from '@/lib/auth';

export function UserForm() {
  const { user } = useUser();
  const canEditRoles = hasPermission(user, 'users.edit.roles');
  
  return (
    <form>
      {/* Form fields */}
      {canEditRoles && <RoleSelector />}
    </form>
  );
}

Performance Considerations

Data Table Virtualization

For large datasets, implement virtualization in the shared data table:
// shared/components/data-table/data-table.tsx
import { useVirtualizer } from '@tanstack/react-virtual';

export function DataTable({ data, columns }) {
  const parentRef = useRef(null);
  
  const virtualizer = useVirtualizer({
    count: data.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  
  return (
    <div ref={parentRef} className="table-container">
      {virtualizer.getVirtualItems().map((virtualRow) => (
        <TableRow key={virtualRow.index} data={data[virtualRow.index]} />
      ))}
    </div>
  );
}

Chart Performance

Memo-ize expensive chart calculations in feature-specific hooks:
// features/analytics/hooks/use-analytics-data.ts
import { useMemo } from 'react';

export function useAnalyticsData() {
  const rawData = useRawData();
  
  const chartData = useMemo(() => {
    // Expensive data transformation
    return transformDataForChart(rawData);
  }, [rawData]);
  
  return { chartData };
}

Testing Strategy

  • Shared components - Unit test thoroughly since they’re used everywhere
  • Feature components - Test in isolation with mocked dependencies
  • Integration tests - Test feature flows end-to-end
  • E2E tests - Test critical user journeys across features

Build docs developers (and LLMs) love