Architecture Overview
This document provides a high-level overview of the React PKL v0.2.0 architecture.
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ Host Application │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Your Custom SDK Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ Context │ │ Slots │ │ Style │ │ │
│ │ │ Definition │ │ Definition │ │ Context │ │ │
│ │ └──────────────┘ └──────────────┘ └────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ React PKL Core │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ Plugin │ │ Theme │ │ Resource │ │ │
│ │ │ Host │ │ Manager │ │ Tracker │ │ │
│ │ └─────────────┘ └──────────────┘ └───────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ React Integration │ │ │
│ │ │ Provider │ LayoutContext │ Hooks │ Slots │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ Uses
▼
┌───────────────────────────────────────┐
│ Plugins │
│ ┌──────────┐ ┌──────────┐ │
│ │ Standard │ │ Theme │ ┌────┐ │
│ │ Plugin │ │ Plugin │ │... │ │
│ └──────────┘ └──────────┘ └────┘ │
└───────────────────────────────────────┘
Core Components
1. PluginHost
Central controller for plugin lifecycle, theme management, and layout slots.
PluginHost<TContext>
├── Theme Management
│ ├── setThemePlugin(plugin)
│ ├── getThemePlugin()
│ └── getLayoutSlotOverride(component)
│
├── Resource Tracking
│ ├── trackResource(cleanup)
│ └── getCurrentPlugin()
│
└── Delegates to PluginManager
├── add()
├── enable()
├── disable()
└── remove()
Responsibilities:
- Manage theme plugin activation/deactivation
- Track layout slot overrides per theme
- Handle resource cleanup for plugins
- Delegate lifecycle operations to PluginManager
- Notify React components of state changes
Theme System:
- Only one theme plugin active at a time
- Theme plugins use
onThemeEnable(slots)to register layout overrides - Theme plugins use
onThemeDisable()for cleanup - Layout slots are stored in
Map<Function, Function>
2. Layout Context & Slots
Context-driven architecture for UI composition.
LayoutContext
├── slots: Map<string, SlotContent[]>
├── layoutSlots: Map<Function, Function>
└── Methods
├── registerSlotItem(name, component)
├── getSlotComponents(name)
└── getLayoutSlotOverride(component)
Layout Slot Pattern:
// Default implementation
export function AppHeader() {
const { toolbar } = useAppLayout();
return <header>{toolbar}</header>;
}
// Theme plugin overrides
plugin.onThemeEnable(slots => {
function DarkHeader() {
const { toolbar } = useAppLayout();
return <header style={{background: '#000'}}>{toolbar}</header>;
}
slots.set(AppHeader, DarkHeader);
});
Key Features:
- Components use hooks instead of props (no prop drilling)
useAppContext()- Access host application contextuseAppLayout()- Access layout slot contentuseAppLayoutSlot()- Check for theme overrides- Theme plugins can replace entire layout components
3. Style Context
Type-safe theming with CSS variables.
StyleContext
├── StyleProvider (component)
├── useStyles() (hook)
└── StyleVariables (interface)
├── bgPrimary
├── textPrimary
├── textSecondary
├── accentColor
├── linkColor
├── borderColor
├── cardBg
├── cardBgSecondary
├── toolbarBg
├── sidebarBg
├── textMuted
└── borderAccent
Usage Pattern:
<StyleProvider variables={{ bgPrimary: '#1a1a1a', textPrimary: '#fff' }}>
<MyComponent />
</StyleProvider>
function MyComponent() {
const styles = useStyles();
return <div style={{ background: styles.bgPrimary }}>...</div>;
}
4. Plugin Types
Standard Plugins
Extend functionality through slots and lifecycle hooks.
export const myPlugin: PluginModule<AppContext> = {
meta: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
activate(context) {
// Initialize, register routes, etc.
},
deactivate() {
// Cleanup
},
entrypoint() {
return <MyComponent />;
}
};
Theme Plugins
Override layout components and provide styling.
export const darkTheme: PluginModule<AppContext> = {
meta: { id: 'dark-theme', name: 'Dark Theme', version: '1.0.0' },
onThemeEnable(slots) {
slots.set(AppHeader, DarkHeader);
slots.set(AppSidebar, DarkSidebar);
slots.set(AppDashboard, DarkDashboard);
// Return cleanup function
return () => {
// Cleanup theme-specific resources
};
},
onThemeDisable() {
// Additional cleanup if needed
}
};
Static Plugins
No lifecycle methods - always available.
export const staticPlugin: PluginModule<AppContext> = {
meta: { id: 'static', name: 'Static Plugin', version: '1.0.0' },
// No activate/deactivate
entrypoint() {
return <ToolbarButton />;
}
};
Data Flow
Plugin Loading & Activation
┌──────────┐ ┌────────────┐ ┌──────────────┐
│ App │─add()→│ PluginHost │─add()→│ PluginManager│
└──────────┘ └──────┬─────┘ └──────┬───────┘
│ │
enable(id) enable(id)
│ │
▼ ▼
┌─────────────┐ ┌────────────┐
│ plugin. │ │ Registry │
│ activate() │ │ Update │
└─────────────┘ └────────────┘
Theme Activation
┌──────────┐ ┌────────────┐
│ App │─setThemePlugin()───→│ PluginHost │
└──────────┘ └──────┬─────┘
│
├─→ Disable current theme
│ ├─→ Call cleanup function
│ └─→ Call onThemeDisable()
│
├─→ Clear layout slot map
│
└─→ Enable new theme
├─→ Call onThemeEnable(slots)
├─→ Store cleanup function
└─→ Notify React components
Hook-Based Component Flow
Component Render
│
├─→ useAppContext()
│ └─→ Access router, plugins, etc.
│
├─→ useAppLayout()
│ └─→ Access slot content (toolbar, sidebar, dashboard)
│
├─→ useAppLayoutSlot(AppHeader)
│ ├─→ Check theme override
│ └─→ Return override or default
│
└─→ useStyles()
└─→ Access theme variables
Resource Cleanup
Plugin State Resource Tracking
│ │
activate() │
├─→ context.router.registerRoute() ┐
├─→ context.events.on(...) ├─→ trackResource()
├─→ window.addEventListener(...) ┘ └─→ Map<plugin, cleanup[]>
│ │
disable() │
└─→ PluginHost.cleanup(plugin) ───────────────┤
▼
Run all cleanup functions
│
├─→ unregisterRoute()
├─→ events.off()
└─→ removeEventListener()
React Integration Architecture
React Component Tree
│
├─ <PluginProvider host={pluginHost}>
│ │
│ ├─ Context: { host, plugins, version }
│ │
│ ├─ <LayoutProvider>
│ │ │
│ │ ├─ Context: { slots, layoutSlots }
│ │ │
│ │ └─ <StyleProvider variables={...}>
│ │ │
│ │ ├─ Context: { styleVariables }
│ │ │
│ │ └─ App Components
│ │ │
│ │ ├─ <Slot name="toolbar" />
│ │ │ │
│ │ │ └─→ Render slot items
│ │ │
│ │ ├─ <LayoutSlot default={AppHeader} />
│ │ │ │
│ │ │ ├─→ Check for theme override
│ │ │ └─→ Render override or default
│ │ │
│ │ └─ Custom Components
│ │ │
│ │ ├─→ useAppContext()
│ │ ├─→ useAppLayout()
│ │ └─→ useStyles()
│ │
│ └─ <PluginEntrypoints />
│ └─→ Render plugin.entrypoint() for each plugin
Key Concepts:
PluginProviderwraps app with plugin contextLayoutProvidermanages slots and layout overridesStyleProviderprovides theme variables- Hooks enable components to access context without prop drilling
- Theme overrides are resolved at render time
Plugin Lifecycle States
add()
┌───────────────────┐
│ Not Loaded │
└─────────┬─────────┘
│
▼
┌───────┐ enable()
│ Added ├─────────────────────┐
└───┬───┘ │
│ ▼
│ ┌──────────┐
│ │ Enabled │
│ │(active) │
│ └────┬─────┘
│ │
│ │ disable()
│ │
│ ▼
│ ┌──────────┐
│ remove() │ Disabled │
└◄──────────────────┤ │
└──────────┘
Special Cases:
- Static Plugin: always "Enabled", cannot be disabled
- Theme Plugin: can be set as active theme independently
Type System Architecture
// Core interfaces
interface PluginModule<TContext> {
meta: PluginMeta;
activate?(context: TContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
entrypoint?(): ReactNode;
onThemeEnable?(slots: Map<Function, Function>): void | (() => void);
onThemeDisable?(): void;
}
// Generic over context type
PluginHost<TContext>
│
├─→ PluginManager<TContext>
│ │
│ └─→ PluginRegistry<TContext>
│ │
│ └─→ PluginEntry<TContext>
│ │
│ └─→ PluginModule<TContext>
│ │
│ └─→ activate(context: TContext)
│
└─→ Theme tracking
│
└─→ Map<Function, Function> (layout slot overrides)
Type Safety:
- Everything is generic over
TContext - Context type defined once in SDK
- Propagates through entire system
- Full TypeScript inference in plugins
- Layout slots are type-safe components
SDK Layer Pattern
Plugin Developer Code
│
│ imports
▼
Your Custom SDK ──────────┐
│ │
│ uses │ defines
▼ ▼
React PKL Core AppContext<YourType>
│ Layout Slots (AppHeader, AppSidebar)
│ Style Context (useStyles)
│ Hooks (useAppContext, useAppLayout)
│
│ provides
▼
Plugin System Infrastructure
Separation of Concerns:
- React PKL Core - Generic plugin and theme system
- Your SDK - Domain-specific APIs/types/slots/styling
- Plugins - Business logic using your SDK
Extension Point (Slot) System
Standard Slots
For adding multiple components to a slot.
Host Application Plugin A Plugin B
│ │ │
│ defines │ provides │ provides
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│"toolbar" │ │Component │ │Component │
│ Slot │ │ A │ │ B │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ <Slot │ │
│ name="toolbar"/> │ │
│ │ │
└──────renders────────┴────────────────────┘
│
▼
[ComponentA, ComponentB]
Layout Slots
For theme plugins to override entire layout components.
Default Theme Dark Theme Plugin
│ │
│ defines │ provides
▼ ▼
┌─────────────┐ ┌──────────────┐
│ AppHeader │ │ DarkHeader │
│ (default) │ │ (override) │
└──────┬──────┘ └──────┬───────┘
│ │
│ │ onThemeEnable(slots)
│ <LayoutSlot │ slots.set(AppHeader, DarkHeader)
│ default={AppHeader}/> │
│ │
└────────────────────────┘
│
▼
Renders DarkHeader
Context-Driven Component Pattern
v0.2.0 introduces hook-based components that don't require props.
Old Pattern (v0.1.0)
// Props needed at every level
<AppHeader toolbar={toolbarItems} />
<AppSidebar routes={pluginRoutes} items={sidebarItems} />
New Pattern (v0.2.0)
// No props - uses hooks
<AppHeader />
<AppSidebar />
// Implementation
function AppHeader() {
const { toolbar } = useAppLayout(); // Get content from context
const { router } = useAppContext(); // Get app context
return <header>{toolbar}</header>;
}
Benefits:
- No prop drilling
- Cleaner component signatures
- Easier to override in themes
- Better encapsulation
Build System Architecture
Plugin Source Code
│
│ entry
▼
┌──────────────┐
│ esbuild │
│ │
│ • Bundle │
│ • Transform │
│ • Minify │
│ • External │
│ SDK deps │
└──────┬───────┘
│
├──► dist/index.js (ESM)
└──► dist/index.js.map
SDK is external:
- Plugins import SDK at runtime
- SDK not bundled into plugin
- Host provides SDK to all plugins
- Avoids version conflicts
Advanced Patterns
Theme with Global Styles
export const darkTheme: PluginModule = {
meta: { id: 'dark', name: 'Dark', version: '1.0.0' },
onThemeEnable(slots) {
// Register layout overrides
slots.set(AppHeader, DarkHeader);
slots.set(AppSidebar, DarkSidebar);
// Inject global styles
const style = document.createElement('style');
style.textContent = `
:root {
--color-bg: #1a1a1a;
--color-text: #e4e4e7;
}
`;
document.head.appendChild(style);
// Return cleanup
return () => {
document.head.removeChild(style);
};
}
};
Nested Style Providers
function DarkHeader() {
return (
<StyleProvider variables={{ bgPrimary: '#000', textPrimary: '#fff' }}>
<header>
<StyleProvider variables={{ accentColor: '#60a5fa' }}>
<Toolbar /> {/* Inherits bgPrimary, textPrimary, accentColor */}
</StyleProvider>
</header>
</StyleProvider>
);
}
Variables cascade down - inner providers override outer ones.
Resource Tracking
export const myPlugin: PluginModule<AppContext> = {
meta: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
activate(context) {
// Register a route
const unregisterRoute = context.router.registerRoute({
path: '/my-page',
component: MyPage
});
context.trackResource(unregisterRoute);
// Add event listener
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
context.trackResource(() => {
window.removeEventListener('resize', handler);
});
// All tracked resources cleaned up when plugin disabled
}
};
Summary
React PKL v0.2.0 architecture is built on these principles:
- Context-Driven - Components use hooks, not props
- Theme System - Plugins can override entire layout with onThemeEnable/onThemeDisable
- Style Context - Type-safe theme variables accessible via useStyles()
- Separation of Concerns - Core system, SDK layer, and plugins are distinct
- Type Safety - Generic types flow through the entire system
- Automatic Cleanup - Resources are tracked and cleaned up automatically
- Static Plugins - Support for plugins without lifecycle management
- Layout Slots - Map-based component override system for themes
The architecture enables:
- ✅ Type-safe plugin and theme development
- ✅ Automatic resource management
- ✅ Zero prop drilling with hooks
- ✅ Flexible theming system
- ✅ Clean separation of concerns
- ✅ Seamless React integration
- ✅ Multiple theme support
- ✅ Style variable cascading
Migration from v0.1.0
If you're upgrading from v0.1.0:
- PluginManager still exists but is wrapped by PluginHost
- Layout components should use hooks instead of props:
- Replace
toolbarprop withuseAppLayout().toolbar - Replace
pluginRoutesprop withuseAppContext().router.getRoutes()
- Replace
- Theme plugins use new
onThemeEnable/onThemeDisablehooks - Style context is new - use for theme variables
- Static plugins are now supported (no activate/deactivate needed)
See THEME_SYSTEM.md for detailed theme system documentation.
add()
┌───────────────────┐
│ Not Loaded │
└─────────┬─────────┘
│
▼
┌───────┐ enable()
│ Added ├─────────────────────┐
└───┬───┘ │
│ ▼
│ ┌──────────┐
│ │ Enabled │
│ │(active) │
│ └────┬─────┘
│ │
│ │ disable()
│ │
│ ▼
│ ┌──────────┐
│ remove() │ Disabled │
└◄──────────────────┤ │
└──────────┘
Type System Architecture
// Generic over context type
interface PluginModule<TContext> {
meta: PluginMeta;
activate?(context: TContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
components?: Record<string, ComponentType<any>>;
}
// Flows through the system
PluginManager<TContext>
│
├─→ PluginRegistry<TContext>
│
└─→ PluginEntry<TContext>
│
└─→ PluginModule<TContext>
│
└─→ activate(context: TContext)
Type Safety:
- Everything is generic over
TContext - Context type defined once in SDK
- Propagates through entire system
- Full type checking in plugins
SDK Layer Pattern
Plugin Developer Code
│
│ imports
▼
Your Custom SDK ──────────┐
│ │
│ uses │ defines
▼ ▼
React PKL Core AppContext<YourType>
│
│ provides
▼
Plugin System Infrastructure
Separation of Concerns:
- React PKL Core - Generic plugin system
- Your SDK - Domain-specific APIs/types
- Plugins - Business logic using your SDK
Extension Point (Slot) System
Host Application Plugin A Plugin B
│ │ │
│ defines │ provides │ provides
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│"toolbar" │ │Component │ │Component │
│ Slot │ │ A │ │ B │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ <PluginSlot │ │
│ name="toolbar"/> │ │
│ │ │
└──────renders────────┴────────────────────┘
│
▼
[ComponentA, ComponentB]
Build System Architecture
Plugin Source Code
│
│ entry
▼
┌──────────────┐
│ esbuild │
│ │
│ • Bundle │
│ • Transform │
│ • Minify │
└──────┬───────┘
│
├──► index.js (ESM/CJS)
├──► index.js.map
└──► meta.json (optional)
Summary
React PKL's architecture is built on these principles:
- Separation of Concerns - Core system, SDK layer, and plugins are distinct
- Type Safety - Generic types flow through the entire system
- Flexibility - Two modes (standalone/client) for different use cases
- Automatic Cleanup - Resources are tracked and cleaned up automatically
- React Native - Deep integration with React through hooks and components
- Extensibility - SDK layer allows customization without forking
The architecture enables:
- ✅ Type-safe plugin development
- ✅ Automatic resource management
- ✅ Flexible deployment models
- ✅ Clean separation of concerns
- ✅ Seamless React integration