React Dashboard Component Architecture

How to architect React dashboard components with a widget system, layout composition, and data fetching patterns — with examples from Klivvr's Web Ops Console.

technical6 min readBy Klivvr Engineering
Share:

Internal operations dashboards have unique architectural requirements. They display diverse data types — metrics, charts, tables, logs, and alerts — from multiple data sources with different refresh rates. They must be customizable, because different teams need different views. And they must be fast, because operations teams use them during incidents when every second matters.

This article covers the component architecture of Klivvr's Web Ops Console, from the widget system that enables flexible layouts to the data fetching patterns that keep the dashboard responsive.

The Widget System

The Web Ops Console is built around a widget system. Each widget is a self-contained component that fetches its own data, manages its own refresh cycle, and renders independently. Widgets can be arranged in customizable layouts.

interface WidgetConfig {
  id: string;
  type: string;
  title: string;
  size: "sm" | "md" | "lg" | "xl";
  refreshInterval?: number;
  dataSource: DataSourceConfig;
  options: Record<string, unknown>;
}
 
interface DataSourceConfig {
  endpoint: string;
  method: "GET" | "POST";
  params?: Record<string, string>;
  transform?: string;
}
 
interface WidgetProps<T = unknown> {
  config: WidgetConfig;
  data: T | null;
  isLoading: boolean;
  error: Error | null;
  onRefresh: () => void;
}
import { useState, useEffect, useCallback } from "react";
 
function Widget<T>({ config, children }: {
  config: WidgetConfig;
  children: (props: WidgetProps<T>) => React.ReactNode;
}) {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  const fetchData = useCallback(async () => {
    setIsLoading(true);
    setError(null);
 
    try {
      const response = await fetch(config.dataSource.endpoint, {
        method: config.dataSource.method,
      });
 
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const result = await response.json();
      setData(result as T);
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsLoading(false);
    }
  }, [config.dataSource]);
 
  useEffect(() => {
    fetchData();
 
    if (config.refreshInterval) {
      const interval = setInterval(fetchData, config.refreshInterval);
      return () => clearInterval(interval);
    }
  }, [fetchData, config.refreshInterval]);
 
  return (
    <div className="widget-container rounded-lg border bg-white shadow-sm">
      <div className="widget-header flex items-center justify-between border-b px-4 py-3">
        <h3 className="text-sm font-medium text-gray-900">{config.title}</h3>
        <div className="flex items-center gap-2">
          {isLoading && <LoadingSpinner size="sm" />}
          <button
            onClick={fetchData}
            className="text-gray-400 hover:text-gray-600"
            title="Refresh"
          >
            <RefreshIcon className="h-4 w-4" />
          </button>
        </div>
      </div>
      <div className="widget-body p-4">
        {children({ config, data, isLoading, error, onRefresh: fetchData })}
      </div>
    </div>
  );
}

Layout Composition

Dashboards are composed of widgets arranged in a grid layout. The layout system supports multiple preset configurations and user customization.

interface DashboardLayout {
  id: string;
  name: string;
  columns: number;
  rows: LayoutRow[];
}
 
interface LayoutRow {
  widgets: Array<{
    widgetId: string;
    colSpan: number;
  }>;
}
 
function DashboardGrid({ layout, widgets }: {
  layout: DashboardLayout;
  widgets: Map<string, WidgetConfig>;
}) {
  return (
    <div
      className="grid gap-4"
      style={{
        gridTemplateColumns: `repeat(${layout.columns}, minmax(0, 1fr))`,
      }}
    >
      {layout.rows.flatMap((row) =>
        row.widgets.map((slot) => {
          const config = widgets.get(slot.widgetId);
          if (!config) return null;
 
          return (
            <div
              key={slot.widgetId}
              style={{ gridColumn: `span ${slot.colSpan}` }}
            >
              <WidgetRenderer config={config} />
            </div>
          );
        })
      )}
    </div>
  );
}
 
function WidgetRenderer({ config }: { config: WidgetConfig }) {
  switch (config.type) {
    case "metric":
      return <MetricWidget config={config} />;
    case "chart":
      return <ChartWidget config={config} />;
    case "table":
      return <TableWidget config={config} />;
    case "log_viewer":
      return <LogViewerWidget config={config} />;
    case "alert_list":
      return <AlertListWidget config={config} />;
    default:
      return <UnknownWidget config={config} />;
  }
}

Metric Widgets

Metric widgets display key numbers with trend indicators — current value, change percentage, and a sparkline showing recent history.

interface MetricData {
  current: number;
  previous: number;
  unit: string;
  history: Array<{ timestamp: string; value: number }>;
}
 
function MetricWidget({ config }: { config: WidgetConfig }) {
  return (
    <Widget<MetricData> config={config}>
      {({ data, isLoading, error }) => {
        if (error) return <ErrorState message={error.message} />;
        if (isLoading || !data) return <MetricSkeleton />;
 
        const change = ((data.current - data.previous) / data.previous) * 100;
        const isPositive = change >= 0;
 
        return (
          <div>
            <div className="flex items-baseline gap-2">
              <span className="text-3xl font-bold text-gray-900">
                {formatNumber(data.current)}
              </span>
              <span className="text-sm text-gray-500">{data.unit}</span>
            </div>
            <div className="mt-1 flex items-center gap-1">
              <span
                className={`text-sm font-medium ${
                  isPositive ? "text-green-600" : "text-red-600"
                }`}
              >
                {isPositive ? "+" : ""}
                {change.toFixed(1)}%
              </span>
              <span className="text-xs text-gray-400">vs previous period</span>
            </div>
            <div className="mt-3 h-12">
              <Sparkline data={data.history} />
            </div>
          </div>
        );
      }}
    </Widget>
  );
}

Data Table Widgets

Table widgets display tabular data with sorting, filtering, and pagination — essential for viewing lists of services, recent deployments, or error logs.

interface TableData {
  columns: Array<{
    key: string;
    label: string;
    sortable: boolean;
    type: "string" | "number" | "date" | "status";
  }>;
  rows: Record<string, unknown>[];
  totalRows: number;
  page: number;
  pageSize: number;
}
 
function TableWidget({ config }: { config: WidgetConfig }) {
  const [sortBy, setSortBy] = useState<string | null>(null);
  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
  const [filter, setFilter] = useState("");
 
  return (
    <Widget<TableData> config={config}>
      {({ data, isLoading, error }) => {
        if (error) return <ErrorState message={error.message} />;
        if (isLoading || !data) return <TableSkeleton />;
 
        const filteredRows = data.rows.filter((row) =>
          Object.values(row).some((v) =>
            String(v).toLowerCase().includes(filter.toLowerCase())
          )
        );
 
        const sortedRows = sortBy
          ? [...filteredRows].sort((a, b) => {
              const aVal = a[sortBy];
              const bVal = b[sortBy];
              const cmp = String(aVal).localeCompare(String(bVal));
              return sortOrder === "asc" ? cmp : -cmp;
            })
          : filteredRows;
 
        return (
          <div>
            <input
              type="text"
              placeholder="Filter..."
              value={filter}
              onChange={(e) => setFilter(e.target.value)}
              className="mb-3 w-full rounded border px-3 py-1.5 text-sm"
            />
            <table className="w-full text-sm">
              <thead>
                <tr className="border-b text-left text-gray-500">
                  {data.columns.map((col) => (
                    <th
                      key={col.key}
                      className="pb-2 pr-4 font-medium cursor-pointer"
                      onClick={() => {
                        if (col.sortable) {
                          setSortBy(col.key);
                          setSortOrder((o) => (o === "asc" ? "desc" : "asc"));
                        }
                      }}
                    >
                      {col.label}
                      {sortBy === col.key && (sortOrder === "asc" ? " ↑" : " ↓")}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {sortedRows.map((row, i) => (
                  <tr key={i} className="border-b last:border-0">
                    {data.columns.map((col) => (
                      <td key={col.key} className="py-2 pr-4">
                        <CellRenderer type={col.type} value={row[col.key]} />
                      </td>
                    ))}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        );
      }}
    </Widget>
  );
}

Conclusion

A dashboard component architecture built on composable widgets provides the flexibility that operations teams need — each team can configure their view with the metrics, charts, tables, and logs relevant to their responsibilities. The Web Ops Console's widget system ensures that each component is self-contained (fetches its own data), independently refreshable (different widgets can have different refresh rates), and composable (widgets can be rearranged without code changes). This architecture scales from a simple metrics overview to a comprehensive operations center as the team's monitoring needs evolve.

Related Articles

business

Build vs Buy for Internal Operations Tools

A framework for deciding whether to build or buy internal operations tools, covering total cost of ownership, customization needs, and the strategic value of purpose-built tooling.

6 min read
technical

Data Visualization Patterns for Ops Dashboards

How to choose and implement effective data visualizations for operations dashboards, covering chart selection, color systems, responsive layouts, and accessibility.

5 min read