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>
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"
}
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>
)
}
ποΈ The Widget Registry
To support multiple widget types, we maintain a registry.
const widgetRegistry = {
"line-chart": LineChartWidget,
"bar-chart": BarChartWidget,
"stat": StatWidget,
"table": TableWidget
}
Adding a new widget type becomes straightforward:
- Create the component\
- 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)
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
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
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 }
}
]
}
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
}
}
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>
)
}
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]
Base Widget Configuration
Every widget shares a common structure.
type BaseWidgetConfig = {
id: string
title: string
description?: string
type: WidgetType
layout: GridLayout
}
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"
}
Union Type for All Widgets
type WidgetConfig =
| LineChartWidgetConfig
| StatWidgetConfig
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)