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:

HookWhen it firesTypical use
onInitOnce, when the plugin is first loaded.Initialise state, fetch data, set up event listeners.
onVisibleEach time the plugin's UI becomes visible.Resume animations, refresh stale data.
onHiddenEach time the plugin's UI is hidden.Pause timers, stop animations, debounce saves.
onDestroyOnce, 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 -> Unload

Registering 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 onVisible call.
  • Can be async -- the runtime waits for the returned Promise to settle before proceeding to onVisible.
  • Has a configurable timeout (default: 30 seconds). If onInit does 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);
},
Do not block onInit

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();
},
Battery and performance

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 onDestroy returns, the plugin's UI container is removed from the DOM and the sandbox is torn down.
  • Any storage.watch subscriptions are automatically cleaned up after onDestroy, 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);
  }
},
Error visibility

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.onVisible

Fullscreen to widget

fullscreen.onHidden -> widget.onVisible

The 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

  1. Keep onInit under 3 seconds. Pre-render a skeleton UI immediately and populate it asynchronously.
  2. Always clean up in onDestroy. Close connections, cancel timers, and remove event listeners.
  3. Pause expensive work in onHidden. Timers, animations, and polling loops should stop when the user cannot see the plugin.
  4. Do not rely on onDestroy for critical saves. The runtime may kill the plugin if onDestroy takes too long. Save incrementally throughout the plugin's lifetime.
  5. Handle errors gracefully. Display a user-friendly fallback instead of leaving the plugin in a broken state.