DEV Community

abhilashlr
abhilashlr

Posted on

Stop Hardcoding Dashboards: Build JSON-Driven Analytics Widgets Instead

Analytics dashboards usually start simple.

A chart here. A table there. Maybe a couple of metrics.

But over time, dashboards grow into something far more complex:
different widget types, customizable titles and descriptions, layout
rules, conditional rendering, and feature flags.

If each widget is hardcoded in the UI, the dashboard slowly turns into a
collection of one-off components that are difficult to maintain.

Recently, we solved this problem by building an analytics widget system
that combines composable React components with a JSON-driven
configuration layer
.

The result: dashboards where widgets are defined by configuration
instead of hardcoded UI
.


⚠️ The Problem with Hardcoded Dashboards

Many dashboards begin with a straightforward implementation.

<Dashboard>
  <RevenueChart />
  <ActiveUsersChart />
  <ErrorRateWidget />
</Dashboard>
Enter fullscreen mode Exit fullscreen mode

This works fine at first, but the problems appear as dashboards evolve:

  • Every widget requires a dedicated component\
  • Layout and presentation logic get duplicated\
  • Titles and descriptions live inside UI code\
  • Small content changes require deployments\
  • Adding variations leads to component explosion

Eventually, the dashboard becomes tightly coupled to the frontend
codebase.

We wanted a system where widgets could be described declaratively
instead of implemented manually
.


πŸ’‘ The Core Idea: Configuration Over Components

Instead of hardcoding widgets in React, we describe them using a JSON
configuration
.

Example:

{
  "id": "api_error_rate",
  "title": "API Error Rate",
  "description": "Error percentage in the last 24 hours",
  "type": "line-chart",
  "query": "errors_over_time",
  "timeRange": "24h"
}
Enter fullscreen mode Exit fullscreen mode

This configuration contains everything required to render a widget.

The UI layer simply interprets the configuration and renders the
correct component
.

This creates a clear separation:

Configuration β†’ what should be shown\
Components β†’ how it should be rendered


🧩 The Widget Renderer

At runtime, a renderer maps the configuration to the appropriate
component.

function WidgetRenderer({ config }) {
  const Component = widgetRegistry[config.type]

  return (
    <WidgetContainer
      title={config.title}
      description={config.description}
    >
      <Component config={config} />
    </WidgetContainer>
  )
}
Enter fullscreen mode Exit fullscreen mode

πŸ—‚οΈ The Widget Registry

To support multiple widget types, we maintain a registry.

const widgetRegistry = {
  "line-chart": LineChartWidget,
  "bar-chart": BarChartWidget,
  "stat": StatWidget,
  "table": TableWidget
}
Enter fullscreen mode Exit fullscreen mode

Adding a new widget type becomes straightforward:

  1. Create the component\
  2. Register it in the widget registry

No changes to the dashboard structure are required.


🧱 A Composable Widget Structure

Every widget follows a consistent layout:

Widget
 β”œβ”€β”€ Title
 β”œβ”€β”€ Description
 └── Content (chart / table / stat)
Enter fullscreen mode Exit fullscreen mode

The container component handles:

  • layout and spacing\
  • titles and descriptions\
  • loading states\
  • error states\
  • consistent styling

This allows widgets to focus purely on data visualization logic.


πŸ” Before vs After: The Architecture Shift

❌ Before: Hardcoded Dashboard

Dashboard
 β”œβ”€β”€ RevenueChart
 β”œβ”€β”€ ActiveUsersChart
 β”œβ”€β”€ ErrorRateWidget
 β”œβ”€β”€ LatencyChart
 └── Many one-off components
Enter fullscreen mode Exit fullscreen mode

Problems:

  • dashboards tightly coupled to UI code
  • adding widgets required new components
  • layout logic lived inside the page
  • small changes required deployments
  • component trees kept growing

βœ… After: Configuration-Driven Dashboard

Dashboard
   β”‚
   β–Ό
Dashboard Config
   β”‚
   β–Ό
Grid Layout Engine
   β”‚
   β–Ό
Widget Renderer
   β”‚
   β–Ό
Widget Registry
   β”‚
   β”œβ”€β”€ LineChartWidget
   β”œβ”€β”€ StatWidget
   β”œβ”€β”€ TableWidget
   └── BarChartWidget
Enter fullscreen mode Exit fullscreen mode

Widgets are no longer hardcoded --- they are rendered dynamically from
configuration
.


πŸ“¦ Dashboard Configuration Example

{
  "widgets": [
    {
      "id": "error_rate",
      "type": "line-chart",
      "title": "API Error Rate",
      "description": "Error percentage over time",
      "query": "errors_over_time",
      "layout": { "x": 0, "y": 0, "w": 6, "h": 4 }
    },
    {
      "id": "success_rate",
      "type": "stat",
      "title": "Success Rate",
      "valueFormat": "percentage",
      "layout": { "x": 6, "y": 0, "w": 3, "h": 2 }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This configuration becomes the single source of truth for the
dashboard.


πŸ“ Adding a Layout System

Rendering widgets is only half the challenge. Dashboards also require
flexible layout management.

To solve this, we integrated React Grid Layout, which provides a
responsive drag‑and‑drop grid system.

Instead of hardcoding layout in UI code, widget positioning is also
defined in configuration.


πŸ“ Grid Layout Configuration

{
  "layout": {
    "x": 0,
    "y": 0,
    "w": 6,
    "h": 4
  }
}
Enter fullscreen mode Exit fullscreen mode

Layout properties:

Property Meaning


x horizontal grid position
y vertical grid position
w widget width in grid units
h widget height in grid units

Separating widget configuration from layout configuration keeps
the system clean and extensible.


🧩 Rendering the Grid

import GridLayout from "react-grid-layout"

function Dashboard({ widgets }) {
  const layout = widgets.map(widget => ({
    i: widget.id,
    ...widget.layout
  }))

  return (
    <GridLayout layout={layout} cols={12} rowHeight={80} width={1200}>
      {widgets.map(widget => (
        <div key={widget.id}>
          <WidgetRenderer config={widget} />
        </div>
      ))}
    </GridLayout>
  )
}
Enter fullscreen mode Exit fullscreen mode

This enables:

  • configuration-driven layouts
  • flexible widget positioning
  • consistent dashboard structure

πŸ”’ Adding Type Safety with TypeScript

When dashboards are driven by configuration, type safety becomes
essential
.

Widget Type Definitions

Centralizing widget types avoids registry/config mismatches.

export const WidgetTypes = {
  LINE_CHART: "line-chart",
  STAT: "stat",
  TABLE: "table"
} as const

export type WidgetType =
  typeof WidgetTypes[keyof typeof WidgetTypes]
Enter fullscreen mode Exit fullscreen mode

Base Widget Configuration

Every widget shares a common structure.

type BaseWidgetConfig = {
  id: string
  title: string
  description?: string
  type: WidgetType
  layout: GridLayout
}
Enter fullscreen mode Exit fullscreen mode

Stable IDs allow:

  • drag‑and‑drop persistence
  • layout tracking
  • widget analytics
  • configuration updates

Widget‑Specific Configurations

Each widget extends the base configuration.

type LineChartWidgetConfig = BaseWidgetConfig & {
  type: "line-chart"
  query: string
  timeRange: "1h" | "24h" | "7d"
}

type StatWidgetConfig = BaseWidgetConfig & {
  type: "stat"
  valueFormat?: "percentage" | "number"
  color?: "green" | "red"
}
Enter fullscreen mode Exit fullscreen mode

Union Type for All Widgets

type WidgetConfig =
  | LineChartWidgetConfig
  | StatWidgetConfig
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • invalid configurations fail at compile time
  • editors provide autocomplete
  • widgets remain type-safe as the system grows

πŸš€ Final Thoughts

Dashboards often start as simple collections of charts but eventually
evolve into complex UI systems.

By combining:

  • JSON-driven widget configuration
  • composable React components
  • a typed widget registry
  • a grid layout system
  • stable widget identities

we created a dashboard architecture that scales without turning into a
collection of one-off components.

Widgets are no longer hardcoded UI elements --- they are structured
configuration interpreted by a flexible UI layer
.

Top comments (0)