Quick Reference
Fast lookup for common tasks and patterns in React PKL.
Installation
# Core package
npm install @pkl.js/react react
# SDK build tools (dev dependency)
npm install --save-dev @pkl.js/react-sdk
Basic Setup
Create Plugin Manager
import { PluginManager } from '@pkl.js/react';
const manager = new PluginManager<MyAppContext>(context);
Load a Plugin
// From local module
await manager.add(() => import('./my-plugin.js'), { enabled: true });
// From object
await manager.add({
meta: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
activate: (ctx) => { /* ... */ },
}, { enabled: true });
Enable/Disable Plugin
await manager.enable('plugin-id');
await manager.disable('plugin-id');
await manager.remove('plugin-id');
React Integration
import { PluginProvider, PluginSlot } from '@pkl.js/react/react';
<PluginProvider registry={manager.registry}>
<PluginSlot name="toolbar" />
</PluginProvider>
Plugin Basics
Minimal Plugin
export default {
meta: {
id: 'com.example.plugin',
name: 'My Plugin',
version: '1.0.0',
},
activate(context) {
console.log('Plugin activated!');
},
components: {
toolbar: () => <div>Hello</div>,
},
};
With TypeScript
import { definePlugin } from 'my-sdk';
export default definePlugin({
meta: { /* ... */ },
activate(context) {
// context is typed!
context.notifications.show('Hello');
},
});
React Hooks
import {
usePlugins,
useEnabledPlugins,
usePlugin,
usePluginMeta,
useSlotComponents,
} from '@pkl.js/react/react';
// Get all plugins
const plugins = usePlugins();
// Get enabled plugins only
const enabled = useEnabledPlugins();
// Get specific plugin
const plugin = usePlugin('plugin-id');
// Get metadata only
const metaList = usePluginMeta();
// Get components for a slot
const toolbarComponents = useSlotComponents('toolbar');
Context Patterns
Basic Context
interface AppContext {
user: { id: string; name: string };
api: {
get<T>(path: string): Promise<T>;
post<T>(path: string, data: any): Promise<T>;
};
}
With Services
interface AppContext {
notifications: NotificationService;
router: RouterService;
storage: StorageService;
}
Resource Cleanup
Register Cleanup
export default definePlugin({
activate(context) {
// Some resource
const ws = new WebSocket('wss://example.com');
// Register cleanup
if (context._resources && context._currentPluginId) {
context._resources.register(context._currentPluginId, () => {
ws.close();
});
}
},
});
Helper Function
// In your SDK
export function onCleanup(context: AppContext, cleanup: () => void) {
if (context._resources && context._currentPluginId) {
context._resources.register(context._currentPluginId, cleanup);
}
}
// In plugins
onCleanup(context, () => ws.close());
Slots
Define Slots
// In your SDK
export const APP_SLOTS = {
TOOLBAR: 'toolbar',
SIDEBAR: 'sidebar',
CONTENT: 'content',
} as const;
Use Slots
// In your app
<PluginSlot name="toolbar" />
<PluginSlot name="sidebar" fallback={<p>No plugins</p>} />
<PluginSlot name="content" componentProps={{ theme: 'dark' }} />
Provide Components
// In plugins
export default definePlugin({
components: {
toolbar: MyToolbarComponent,
sidebar: MySidebarComponent,
},
});
Events
Event Bus
// In your SDK
export class EventBus {
on(event: string, handler: Function): () => void;
emit(event: string, data: any): void;
off(event: string, handler: Function): void;
}
// In context
interface AppContext {
events: EventBus;
}
In Plugins
export default definePlugin({
activate(context) {
// Listen
const unsubscribe = context.events.on('user-updated', (user) => {
console.log('User:', user);
});
// Emit
context.events.emit('plugin-ready', { pluginId: 'my-plugin' });
},
});
Route Registration
SDK Implementation
interface RouterService {
registerRoute(route: {
path: string;
component: ComponentType;
label?: string;
}): void;
}
Plugin Usage
export default definePlugin({
activate(context) {
context.router.registerRoute({
path: '/my-page',
component: MyPage,
label: 'My Page',
});
// Auto-cleanup when plugin disabled!
},
});
Building Plugins
Basic Build
import { buildPlugin } from '@pkl.js/react-sdk';
await buildPlugin({
entry: './src/index.tsx',
outDir: './dist',
meta: {
id: 'my-plugin',
name: 'My Plugin',
version: '1.0.0',
},
});
Production Build
await buildPlugin({
entry: './src/index.tsx',
outDir: './dist',
meta: { /* ... */ },
formats: ['esm'],
minify: true,
sourcemap: true,
external: ['lodash'],
});
Client Mode
Setup Client
import { PluginClient } from '@pkl.js/react';
const client = new PluginClient({
manifestUrl: 'https://api.example.com/plugins/manifest.json',
context: myAppContext,
});
await client.sync();
Manifest Format
[
{
"meta": {
"id": "com.example.plugin",
"name": "My Plugin",
"version": "1.0.0"
},
"url": "https://cdn.example.com/plugins/my-plugin/index.js"
}
]
TypeScript Helpers
Define Plugin Helper
// In your SDK
import type { PluginModule } from '@pkl.js/react';
export type MyAppPlugin = PluginModule<MyAppContext>;
export function definePlugin(plugin: MyAppPlugin): MyAppPlugin {
return plugin;
}
Typed Slots
interface SlotProps {
toolbar: { compact?: boolean };
sidebar: { collapsed?: boolean };
}
export type SlotComponents = {
[K in keyof SlotProps]?: ComponentType<SlotProps[K]>;
};
// Use in plugins
export default definePlugin({
components: {
toolbar: ({ compact }) => <div />,
} satisfies SlotComponents,
});
Common Patterns
Lazy Loading
// Load only when needed
if (user.role === 'admin') {
await manager.add(() => import('./admin-plugin.js'), { enabled: true });
}
Plugin State
export default definePlugin({
activate(context) {
const state = context.storage.get('plugin-state') || {};
// Use state...
},
});
Multiple Routes
export default definePlugin({
activate(context) {
const routes = [
{ path: '/page1', component: Page1 },
{ path: '/page2', component: Page2 },
];
routes.forEach(route => context.router.registerRoute(route));
},
});
Conditional Components
export default definePlugin({
components: {
toolbar: ({ user }) => user?.role === 'admin'
? <AdminButton />
: null,
},
});
Testing
Unit Test
import { describe, it, expect, vi } from 'vitest';
import plugin from './my-plugin';
describe('MyPlugin', () => {
it('should activate', async () => {
const mockContext = {
notifications: { show: vi.fn() },
};
await plugin.activate(mockContext);
expect(mockContext.notifications.show).toHaveBeenCalled();
});
});
Integration Test
import { render } from '@testing-library/react';
import { PluginProvider, PluginSlot } from '@pkl.js/react/react';
import { PluginManager } from '@pkl.js/react';
it('renders plugin', async () => {
const manager = new PluginManager();
await manager.add(myPlugin, { enabled: true });
const { getByText } = render(
<PluginProvider registry={manager.registry}>
<PluginSlot name="toolbar" />
</PluginProvider>
);
expect(getByText('Plugin Content')).toBeInTheDocument();
});
Debugging
Check Plugin Status
// Get all plugins
console.log(manager.getAll());
// Get enabled plugins
console.log(manager.getEnabled());
// Get specific plugin
console.log(manager.registry.get('plugin-id'));
Subscribe to Events
manager.subscribe((event) => {
console.log('Plugin event:', event);
});
Check Resources
// Check if plugin has resources
console.log(manager.resources.has('plugin-id'));
Common Issues
Plugin Not Rendering
- Check plugin is enabled:
manager.getEnabled() - Verify slot name matches
- Check component is exported
- Look for errors in console
Type Errors
- Import from your SDK, not
@pkl.js/react - Ensure SDK is built
- Check
tsconfig.jsonsettings
Cleanup Not Working
- Verify
_resourcesis in context - Check
_currentPluginIdis set during activate - Ensure cleanup is registered before activate returns