- codegen: Low-code React app with JSON-driven component system - packagerepo: Schema-driven package repository with backend/frontend - postgres: Next.js app with Drizzle ORM and PostgreSQL Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.4 KiB
Config-Driven Architecture Guide
Overview
This repository has been refactored to use a config-driven architecture where most of the React component structure, wiring, and actions are defined in src/config/features.json rather than in JSX/TSX files. This approach:
- Reduces boilerplate code - Most UI wiring is done via configuration
- Improves maintainability - Changes to UI structure can be made in JSON
- Enables rapid prototyping - New features can be scaffolded from config
- Promotes reusability - Atomic components and hooks are truly reusable
- Simplifies testing - Playbooks defined in JSON for E2E tests
Architecture Components
1. Component Tree Renderer (src/utils/componentTreeRenderer.tsx)
The core of the config-driven architecture. It reads component tree definitions from features.json and dynamically renders React components.
Features:
- Template interpolation:
{{variable}}syntax - Conditional rendering:
conditionproperty - Loops:
forEachproperty for arrays - Action mapping: Maps string action names to functions
- Icon rendering: Automatic Material-UI icon resolution
Example usage:
import { ComponentTreeRenderer } from '@/utils/componentTreeRenderer';
import { getComponentTree } from '@/utils/featureConfig';
const tree = getComponentTree('DashboardStatsCards');
<ComponentTreeRenderer
tree={tree}
context={{
data: { statsCards: [...] },
actions: { handleClick: () => {} },
state: { isOpen: false }
}}
/>
2. Hooks (src/hooks/)
Small, focused hooks for data fetching and business logic:
useApiCall
Generic hook for API calls with loading/error states.
const { data, loading, error, execute } = useApiCall();
// Execute API call
await execute('/api/endpoint', {
method: 'POST',
body: { data: 'value' }
});
useTables
Manage database tables (list, create, drop).
const { tables, loading, error, createTable, dropTable } = useTables();
// Create a table
await createTable('users', [
{ name: 'id', type: 'INTEGER' },
{ name: 'name', type: 'VARCHAR' }
]);
useTableData
Fetch and manage table data.
const { data, loading, error, fetchTableData } = useTableData('users');
useColumnManagement
Column operations (add, modify, drop).
const { addColumn, modifyColumn, dropColumn } = useColumnManagement();
await addColumn('users', {
columnName: 'email',
dataType: 'VARCHAR'
});
3. Features Configuration (src/config/features.json)
The central configuration file containing:
Component Trees (componentTrees)
Define entire component hierarchies:
{
"componentTrees": {
"DashboardStatsCards": {
"component": "Grid",
"props": { "container": true, "spacing": 3 },
"children": [
{
"component": "Grid",
"forEach": "statsCards",
"props": { "item": true, "xs": 12, "sm": 6, "md": 3 },
"children": [
{
"component": "Card",
"children": [...]
}
]
}
]
}
}
}
Component Props (componentProps)
Schema definitions for all available components:
{
"componentProps": {
"Button": {
"description": "Material-UI Button component",
"category": "inputs",
"props": {
"text": { "type": "string", "description": "Button text" },
"variant": {
"type": "enum",
"values": ["text", "outlined", "contained"],
"default": "text"
}
}
}
}
}
Playwright Playbooks (playwrightPlaybooks)
E2E test scenarios defined in JSON:
{
"playwrightPlaybooks": {
"createTable": {
"name": "Create Table Workflow",
"description": "Test creating a new database table",
"tags": ["admin", "table", "crud"],
"steps": [
{ "action": "goto", "url": "/admin/dashboard" },
{ "action": "click", "selector": "text=Table Manager" },
{ "action": "click", "selector": "button:has-text('Create Table')" }
]
}
}
}
Storybook Stories (storybookStories)
Storybook story configurations:
{
"storybookStories": {
"DataGrid": {
"default": {
"name": "Default",
"args": {
"columns": [...],
"rows": [...]
}
}
}
}
}
How to Use This Architecture
Creating a New Config-Driven Component
- Define the component tree in
features.json:
{
"componentTrees": {
"MyNewComponent": {
"component": "Box",
"props": { "sx": { "p": 2 } },
"children": [
{
"component": "Typography",
"props": {
"variant": "h5",
"text": "{{data.title}}"
}
},
{
"component": "Button",
"props": {
"variant": "contained",
"text": "Click Me",
"onClick": "handleClick"
}
}
]
}
}
}
- Create a thin wrapper component:
'use client';
import { getComponentTree } from '@/utils/featureConfig';
import { ComponentTreeRenderer } from '@/utils/componentTreeRenderer';
export default function MyNewComponent() {
const tree = getComponentTree('MyNewComponent');
const actions = {
handleClick: () => console.log('Clicked!'),
};
const data = {
title: 'My Title',
};
return (
<ComponentTreeRenderer
tree={tree}
context={{ data, actions, state: {} }}
/>
);
}
Refactoring an Existing Component
Before:
export default function TableManager({ tables, onCreateTable, onDropTable }) {
return (
<Box>
<Typography variant="h5">Table Management</Typography>
<Button onClick={() => onCreateTable()}>Create Table</Button>
<List>
{tables.map(table => (
<ListItem key={table.name}>
<ListItemText primary={table.name} />
</ListItem>
))}
</List>
</Box>
);
}
After:
export default function TableManager() {
const tree = getComponentTree('TableManagerTab');
const { tables, createTable } = useTables();
const actions = {
openCreateDialog: () => { /* ... */ },
};
return (
<ComponentTreeRenderer
tree={tree}
context={{
data: { tables },
actions,
state: {}
}}
/>
);
}
Testing
Unit Tests
Tests are co-located with the code in src/:
src/hooks/useApiCall.test.ts- Hook logic testssrc/utils/componentTreeRenderer.test.tsx- Renderer tests
Run tests:
npm test
E2E Tests
Playwright tests use playbook definitions from features.json:
import { getAllPlaywrightPlaybooks } from '@/utils/featureConfig';
const playbooks = getAllPlaywrightPlaybooks();
const playbook = playbooks.createTable;
// Execute playbook steps...
Run E2E tests:
npm run test:e2e
Examples
See src/components/examples/ for working examples:
- DashboardStatsExample.tsx - Stats cards rendered from config
- ConfigDrivenTableManager.tsx - Full table management from config
Benefits of This Approach
- Less Code: 70%+ reduction in component code
- Easier Testing: Playbooks in JSON, reusable test utilities
- Better Type Safety: Config schemas with TypeScript types
- Rapid Prototyping: New features scaffolded from config
- Consistent UI: All components follow same patterns
- Easy Refactoring: Change UI structure without touching code
Best Practices
-
Keep wrapper components thin - They should only:
- Fetch/manage data (via hooks)
- Define action handlers
- Call ComponentTreeRenderer
-
Use hooks for business logic - All data fetching, state management, and side effects
-
Define reusable component trees - Break down complex UIs into smaller trees
-
Validate configs - Use
validateComponentProps()to check component definitions -
Document in features.json - Add descriptions to all config entries
Migration Strategy
For existing components:
- Extract business logic to hooks
- Define component tree in features.json
- Replace JSX with ComponentTreeRenderer
- Add tests
- Verify functionality
- Remove old code
Future Enhancements
- Visual config editor
- Real-time config validation
- Component tree visualization
- Auto-generated Storybook stories from config
- Config versioning and migrations