Plugin Lifecycle
Understanding the plugin lifecycle is essential for building robust WebArcade plugins.
Lifecycle Overview
┌─────────────────────────────────────────────────────────────┐
│ Plugin Loaded │
│ ↓ │
│ start(api) ─── Register components, shortcuts, services │
│ ↓ │
│ [Plugin is running] │
│ ↓ │
│ stop(api) ─── Plugin disabled/unloaded │
└─────────────────────────────────────────────────────────────┘Lifecycle Hooks
start(api)
Called once when the plugin is first loaded. Use this to register all components and set up services.
jsx
start(api) {
// Register panel components
api.register('explorer', {
type: 'panel',
component: Explorer,
label: 'Explorer',
icon: IconFolder
});
api.register('editor', {
type: 'panel',
component: Editor,
label: 'Editor'
});
// Register toolbar buttons
api.register('save-btn', {
type: 'toolbar',
icon: IconSave,
tooltip: 'Save (Ctrl+S)',
onClick: () => saveFile()
});
// Register menu items
api.register('file-menu', {
type: 'menu',
label: 'File',
submenu: [
{ id: 'new', label: 'New', action: () => newFile() }
]
});
// Register keyboard shortcuts
api.shortcut({
'ctrl+s': () => saveFile(),
'ctrl+n': () => newFile()
});
// Provide services for other plugins
api.provide('editor', {
openFile: (path) => openFile(path),
save: () => saveFile()
});
// Initialize state
api.set('editor.currentFile', null);
}stop(api)
Called when the plugin is disabled or the application closes.
jsx
stop(api) {
// Save state to disk
saveState();
// Clean up subscriptions
unsubscribeAll();
// Close connections
closeConnections();
}Component Lifecycle Hooks
Individual panel components can have their own lifecycle hooks:
jsx
api.register('editor', {
type: 'panel',
component: Editor,
label: 'Editor',
onMount: () => {
// Called when panel is first rendered
console.log('Editor mounted');
loadLastOpenFile();
},
onUnmount: () => {
// Called when panel is removed
console.log('Editor unmounted');
saveCurrentFile();
},
onFocus: () => {
// Called when panel receives focus
console.log('Editor focused');
},
onBlur: () => {
// Called when panel loses focus
console.log('Editor blurred');
}
});Best Practices
Do in start()
- Register all UI components
- Set up keyboard shortcuts
- Provide services for other plugins
- Subscribe to events from other plugins
- Initialize default state
- Register layouts if providing custom layouts
Do in stop()
- Save persistent state
- Unsubscribe from events
- Clean up connections
- Remove any global listeners
Example: Complete Plugin
jsx
import { plugin } from 'webarcade';
import { createSignal } from 'solid-js';
import { IconNotes, IconPlus, IconTrash } from '@tabler/icons-solidjs';
const [notes, setNotes] = createSignal([]);
const [selectedNote, setSelectedNote] = createSignal(null);
function NoteList() {
return (
<div class="p-4">
<For each={notes()}>
{(note) => (
<div
class={`p-2 cursor-pointer ${selectedNote()?.id === note.id ? 'bg-primary' : ''}`}
onClick={() => setSelectedNote(note)}
>
{note.title}
</div>
)}
</For>
</div>
);
}
function NoteEditor() {
return (
<div class="p-4">
<Show when={selectedNote()} fallback={<p>Select a note</p>}>
<h1 class="text-xl font-bold">{selectedNote().title}</h1>
<textarea
class="w-full h-64 mt-4"
value={selectedNote().content}
onInput={(e) => updateNote(selectedNote().id, e.target.value)}
/>
</Show>
</div>
);
}
function createNote() {
const newNote = {
id: Date.now(),
title: 'New Note',
content: ''
};
setNotes([...notes(), newNote]);
setSelectedNote(newNote);
}
function deleteNote(id) {
setNotes(notes().filter(n => n.id !== id));
if (selectedNote()?.id === id) {
setSelectedNote(null);
}
}
function updateNote(id, content) {
setNotes(notes().map(n =>
n.id === id ? { ...n, content } : n
));
}
export default plugin({
id: 'notes',
name: 'Notes',
version: '1.0.0',
start(api) {
// Load saved notes
const savedNotes = api.get('notes.list', []);
setNotes(savedNotes);
// Register panels
api.register('note-list', {
type: 'panel',
component: NoteList,
label: 'Notes',
icon: IconNotes
});
api.register('note-editor', {
type: 'panel',
component: NoteEditor,
label: 'Editor'
});
// Register toolbar
api.register('new-note', {
type: 'toolbar',
icon: IconPlus,
tooltip: 'New Note',
onClick: createNote
});
api.register('delete-note', {
type: 'toolbar',
icon: IconTrash,
tooltip: 'Delete Note',
onClick: () => selectedNote() && deleteNote(selectedNote().id),
disabled: () => !selectedNote()
});
// Keyboard shortcuts
api.shortcut({
'ctrl+n': createNote,
'delete': () => selectedNote() && deleteNote(selectedNote().id)
});
// Provide service for other plugins
api.provide('notes', {
create: createNote,
delete: deleteNote,
getAll: () => notes(),
getSelected: () => selectedNote()
});
},
stop(api) {
// Save notes before unloading
api.set('notes.list', notes());
}
});Layout Events
Listen for layout changes if your plugin needs to react:
jsx
start(api) {
// Listen for layout changes
document.addEventListener('layout:change', (event) => {
const { from, to } = event.detail;
console.log(`Layout changed from ${from} to ${to}`);
});
}Plugin State Management
Use the shared store for persistent state across sessions:
jsx
start(api) {
// Load previous state
const savedState = api.get('myPlugin.state', {
lastOpenFile: null,
recentFiles: [],
settings: { autoSave: true }
});
// Initialize with saved state
initializeApp(savedState);
// Watch for changes from other plugins
api.watch('settings.theme', (theme) => {
applyTheme(theme);
});
}
stop(api) {
// Save state for next session
api.set('myPlugin.state', getCurrentState());
}Error Handling
Handle errors gracefully in lifecycle hooks:
jsx
start(api) {
try {
// Initialize plugin
initializeComponents(api);
} catch (error) {
console.error('[MyPlugin] Initialization failed:', error);
// Register minimal error UI
api.register('error-view', {
type: 'panel',
component: () => <div class="p-4 text-error">Plugin failed to load</div>,
label: 'Error'
});
}
}