Skip to main content

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

  1. Check plugin is enabled: manager.getEnabled()
  2. Verify slot name matches
  3. Check component is exported
  4. Look for errors in console

Type Errors

  1. Import from your SDK, not @pkl.js/react
  2. Ensure SDK is built
  3. Check tsconfig.json settings

Cleanup Not Working

  1. Verify _resources is in context
  2. Check _currentPluginId is set during activate
  3. Ensure cleanup is registered before activate returns

Further Reading