Lifecycle Hooks
Ghosty plugins are not always visible on screen. A widget might be scrolled out of view, the user might switch away from the fullscreen view, or the application might be shutting down. Lifecycle hooks give your plugin the ability to react to these state transitions and manage resources efficiently.
Overview
Every plugin can register four lifecycle hooks:
| Hook | When it fires | Typical use |
|---|---|---|
onInit | Once, when the plugin is first loaded. | Initialise state, fetch data, set up event listeners. |
onVisible | Each time the plugin's UI becomes visible. | Resume animations, refresh stale data. |
onHidden | Each time the plugin's UI is hidden. | Pause timers, stop animations, debounce saves. |
onDestroy | Once, when the plugin is being unloaded. | Clean up resources, persist final state, close connections. |
The lifecycle follows this flow:
Load -> onInit -> onVisible
|
v
[User interacts]
|
v
onHidden <-> onVisible (repeats)
|
v
onDestroy -> UnloadRegistering hooks
Register hooks by calling ghosty.lifecycle.register() in your entry file.
Each hook is optional; only register the ones you need.
import { ghosty } from "@ghosty/sdk";
ghosty.lifecycle.register({
onInit: async () => {
// Runs once on first load
console.log("Plugin initialised");
},
onVisible: () => {
// Runs every time the plugin becomes visible
console.log("Plugin is now visible");
},
onHidden: () => {
// Runs every time the plugin is hidden
console.log("Plugin is now hidden");
},
onDestroy: async () => {
// Runs once before the plugin is removed
console.log("Plugin is being destroyed");
},
});onInit
The onInit hook fires exactly once when the Ghosty runtime first loads your
plugin. This is the earliest point at which the GhostySDK is available.
Key characteristics:
- Runs before the first
onVisiblecall. - Can be
async-- the runtime waits for the returned Promise to settle before proceeding toonVisible. - Has a configurable timeout (default: 30 seconds). If
onInitdoes not resolve within the timeout, the plugin is killed and an error is logged. - The plugin's UI container is already attached to the DOM but is not yet visible to the user.
Common patterns
onInit: async () => {
// 1. Load saved state from storage
const savedCity = await ghosty.storage.get("city");
// 2. Fetch fresh data from an external API
const weather = await fetchWeather(savedCity ?? "London");
// 3. Pre-render the initial UI into the container
renderWidget(weather);
},Keep onInit fast. Defer heavy work (large data fetches, complex
computations) to after the initial render. A slow onInit delays the
user's first paint and may trigger the timeout.
onVisible
The onVisible hook fires every time the plugin's UI transitions from hidden
to visible. This includes the first visibility after onInit completes and
every subsequent reveal (e.g. the user scrolls the widget back into view or
navigates to the fullscreen view).
Key characteristics:
- Can fire many times during a plugin's lifetime.
- Is synchronous -- if you need to do async work, fire it and forget (do
not make the function itself
async). - The UI container is guaranteed to be in the DOM and visible.
Common patterns
let refreshTimer: number | null = null;
onVisible: () => {
// Resume a periodic refresh timer
refreshTimer = window.setInterval(() => {
refreshData();
}, 60_000);
// Resume CSS animations
document.querySelector(".spinner")?.classList.remove("paused");
},onHidden
The onHidden hook fires every time the plugin's UI transitions from visible
to hidden. This happens when the user scrolls the widget off-screen, switches
to a different Ghosty tab, or minimises the application.
Key characteristics:
- Symmetric counterpart to
onVisible. - Is synchronous.
- The UI container is still in the DOM but is no longer visible.
Common patterns
onHidden: () => {
// Pause the refresh timer to save resources
if (refreshTimer !== null) {
clearInterval(refreshTimer);
refreshTimer = null;
}
// Pause CSS animations
document.querySelector(".spinner")?.classList.add("paused");
// Debounce-save any unsaved user input
saveCurrentFormState();
},Always pause timers and animations in onHidden. Ghosty monitors resource
usage and may throttle or kill plugins that consume CPU while hidden.
onDestroy
The onDestroy hook fires exactly once when the plugin is being unloaded.
This can happen when the user uninstalls the plugin, closes the Ghosty
application, or when the runtime needs to reclaim memory.
Key characteristics:
- Can be
async-- the runtime waits up to the configured timeout for the returned Promise to settle. - After
onDestroyreturns, the plugin's UI container is removed from the DOM and the sandbox is torn down. - Any
storage.watchsubscriptions are automatically cleaned up afteronDestroy, but it is good practice to unsubscribe explicitly.
Common patterns
onDestroy: async () => {
// 1. Persist any unsaved state
await ghosty.storage.set("last_state", getCurrentState());
// 2. Close WebSocket connections
socket?.close();
// 3. Cancel pending fetch requests
abortController?.abort();
// 4. Remove global event listeners
window.removeEventListener("resize", handleResize);
},Error handling in hooks
If a lifecycle hook throws an error (or an async hook rejects), the Ghosty runtime logs the error and continues. The plugin is not killed by a single hook error, but repeated errors may cause the runtime to flag the plugin for review.
onInit: async () => {
try {
const data = await fetchCriticalData();
renderWidget(data);
} catch (error) {
// Render a user-friendly fallback instead of crashing
renderErrorState("Unable to load data. Please try again later.");
console.error("onInit failed:", error);
}
},Hook errors are logged to the browser console in development mode and reported to the Ghosty developer dashboard in production. Check your dashboard regularly for runtime errors.
Lifecycle and view transitions
When a plugin transitions between widget and fullscreen views, the lifecycle hooks fire in a specific order:
Widget to fullscreen
widget.onHidden -> fullscreen.onInit (first time) -> fullscreen.onVisibleFullscreen to widget
fullscreen.onHidden -> widget.onVisibleThe widget view is not destroyed when the user opens fullscreen. It remains
in memory with its state preserved, and onVisible fires again when the user
returns to the dashboard.
Best practices
- Keep
onInitunder 3 seconds. Pre-render a skeleton UI immediately and populate it asynchronously. - Always clean up in
onDestroy. Close connections, cancel timers, and remove event listeners. - Pause expensive work in
onHidden. Timers, animations, and polling loops should stop when the user cannot see the plugin. - Do not rely on
onDestroyfor critical saves. The runtime may kill the plugin ifonDestroytakes too long. Save incrementally throughout the plugin's lifetime. - Handle errors gracefully. Display a user-friendly fallback instead of leaving the plugin in a broken state.