From 03447c897dd5bafb0a07cec91bf94d34e0aa9005 Mon Sep 17 00:00:00 2001 From: Fevol <8179397+fevol@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:34:13 +0100 Subject: [PATCH 1/9] docs: add a guide for using components --- en/Plugins/Guides/Using Components.md | 183 ++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 en/Plugins/Guides/Using Components.md diff --git a/en/Plugins/Guides/Using Components.md b/en/Plugins/Guides/Using Components.md new file mode 100644 index 00000000..796e17f7 --- /dev/null +++ b/en/Plugins/Guides/Using Components.md @@ -0,0 +1,183 @@ +--- +permalink: plugins/guides/using-components +--- + +The `Component` class is a useful tool for managing the lifecycle of UI elements and other classes in Obsidian plugins. It provides a consistent way to handle setup and teardown logic, and helps ensure that your plugin cleans up resources correctly when it is unloaded. + +In this guide, we'll explore how to use the `Component` class effectively in your Obsidian plugins. +For more details about the `Component` class, see the [API reference](/Reference/TypeScript+API/Component). + +## Overview + +Under the hood, the `Plugin`, `View`, and `Menu` classes all extend from `Component`. +This means that they share the same lifecycle management patterns for loading, unloading, and managing child components. + +By using `Component` in your own classes, you can: + +- Organize your plugin logic into modular, self-contained components. +- Automatically clean up event listeners, DOM elements, and intervals. +- Avoid memory leaks and dangling references when unloading. +- Manage external or heavy resources, such as libraries, WebAssembly modules or network clients, safely. + +A `Component` is considered **loaded** after its `load()` method has been called, and **unloaded** after its `unload()` method completes. When a component is unloaded, all of its registered resources and child components are automatically cleaned up. + + +## Adding a window resize listener + +Imagine you want to respond to window resize events in your plugin. +A naïve approach might look like this: + +```ts +// Bad +class MyPlugin extends Plugin { + onload() { + window.addEventListener("resize", onResize); + } +} +``` + +However, when the user disables your plugin, the resize listener will continue to run until Obsidian is restarted. +This can lead to unnecessary memory usage or even errors if your handler references unloaded data. + +To fix this, you might manually remove the listener in `onunload`: + +```ts +// Better +class MyPlugin extends Plugin { + onload() { + window.addEventListener("resize", onResize); + } + + onunload() { + window.removeEventListener("resize", onResize); + } +} +``` + +While this works, it is easy to forget or duplicate cleanup code across different parts of your plugin. +Fortunately, since the `Plugin` class extends `Component`, you can use the `registerDomEvent` helper to handle this automatically: + +```ts +// Good +class MyPlugin extends Plugin { + onload() { + this.registerDomEvent(window, "resize", onResize); + } +} +``` + +Whenever your plugin is unloaded, Obsidian will automatically detach the resize listener, this way you don't have to worry about it! + + + +## Handling different types of events + +In addition to DOM events, `Component` provides helper methods for managing other kinds of resources that need cleanup, such as event listeners within Obsidian itself or active intervals. + +```ts +class MyPlugin extends Plugin { + onload() { + // Register an event listener from Obsidian's `Events` system + this.registerEvent(this.app.vault.on("modify", onFileModify, this)); + + // Register an interval (automatically cleared on unload) + const intervalId = setInterval(onIntervalTick, 1000); + this.registerInterval(intervalId); + } +} +``` + +These registration methods ensure that all listeners and intervals are released when your component unloads. +You can use `register()` for generic cleanup callbacks that don't fall into any of these categories. + + +## Defining your own components + +Beyond `Plugin`, `View`, and other built-in types, you can define your own classes that extend `Component`. +This makes it easy to encapsulate behavior and manage resources for reusable parts of your plugin, such as controllers, widgets, or data managers. + +Here's an example that renders some Markdown in a DOM element, initializes an imaginary external library, and handles window resize events: + +```ts +import { Component } from "obsidian"; +import SomeLibrary from "some-lib"; + +class MyWidget extends Component { + private container: HTMLElement; + private lib: SomeLibrary; + + constructor(private app: App) { + super(); + } + + onload() { + // Add an element to the DOM and render Markdown in it + // The MarkdownRenderer takes the current component as its parent, and adds itself as a child component. + this.container = document.createDiv({ cls: "my-widget" }); + MarkdownRenderer.render(this.app, "## Some Markdown", this.container, "", this); + document.body.appendChild(this.container); + + // Initialize an external resource + this.lib = new SomeLibrary(); + this.lib.initialize(); + + // Handle window resize automatically + this.registerDomEvent(window, "resize", () => { + this.lib.resize(window.innerWidth, window.innerHeight); + }); + } + + doSomething() { + this.lib.doSomething(); + } + + onunload() { + // Clean up when the widget is unloaded + this.container.remove(); + this.lib.destroy(); + } +} +``` + +This widget encapsulates its own lifecycle, initializing when loaded, and cleaning up when unloaded. + +Now, you can attach your `MyWidget` component to your plugin or other components, forming a clear hierarchy of resources. +When a parent component is loaded or unloaded, all of its child components are automatically loaded or unloaded too. + +```ts +class MyPlugin extends Plugin { + private widget: MyWidget; + + onload() { + // Add the widget as a child component + this.widget = this.addChild(new MyWidget(this.app)); + + // And use it as needed + this.widget.doSomething(); + } + + onunload() { + // The widget will automatically unload with the plugin + } + + temporarilyUnloadWidget() { + // You can also unload it temporarily if needed + this.widget.unload(); + window.setTimeout(() => this.widget.load(), 5000); + } + + removeWidget() { + // You can also explicitly remove it at any time + this.removeChild(this.widget); + } +} +``` + +## Summary + +Using the `Component` class allows your plugin to safely manage lifecycle-bound resources in a structured way. +By taking advantage of built-in helpers like `registerDomEvent`, `registerEvent`, and `registerInterval`, you can ensure that your plugin unloads cleanly without memory leaks or dangling references. + +When designing your plugin's architecture, consider extending or composing `Component` objects whenever you need lifecycle-aware behavior. +This will help keep your code modular, predictable, and easy to maintain. + From ab42d4f01a813d6dfe7e41f72113d87a88b9bf5a Mon Sep 17 00:00:00 2001 From: Fevol <8179397+fevol@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:49:32 +0100 Subject: [PATCH 2/9] docs: refactor guide to be more general (lifecycle management) --- en/Plugins/Guides/Manage plugin lifecycle.md | 293 +++++++++++++++++++ en/Plugins/Guides/Using Components.md | 183 ------------ 2 files changed, 293 insertions(+), 183 deletions(-) create mode 100644 en/Plugins/Guides/Manage plugin lifecycle.md delete mode 100644 en/Plugins/Guides/Using Components.md diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md new file mode 100644 index 00000000..6e272ea9 --- /dev/null +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -0,0 +1,293 @@ +--- +permalink: plugins/guides/lifecycle-management +--- + +When developing a plugin, you will often create resources such as event listeners, DOM elements, intervals or workers. While setting these up is often straightforward, it is equally important to properly clean them up when your plugin is unloaded. Neglecting this can lead to memory leaks, dangling event handlers, or unexpected behavior. + + +## What you'll learn + +After completing this guide, you'll be able to: + +- Manage the lifecycle of resources in your plugin. +- Use the `Component` class to simplify cleanup and organization. +- Avoid common pitfalls that lead to resource leaks or stale data. +- Understand how components and subcomponents interact in Obsidian. + + +## Overview + +Imagine your plugin needs to respond to window resize events. +A simple (but incorrect implementation!) might look like this: + +```ts +// Bad +class MyPlugin extends Plugin { + onload() { + window.addEventListener("resize", onResize); + } +} +``` + +This works, but it introduces a problem: when the user disabled your plugin, the resize listener remains active! Only when Obsidian is restarted will it be removed. While this might be a fairly innocuous example, in more complex scenarios, this can lead to errors and memory leaks. + +A more responsible solution would be to manually remove the listener during `onunload`: + +```ts +// Better +class MyPlugin extends Plugin { + onload() { + window.addEventListener("resize", onResize); + } + + onunload() { + window.removeEventListener("resize", onResize); + } +} +``` + +While this works, it quickly becomes repetitive and error-prone. Fortunately, Obsidian’s `Component` system offers a better solution. + + +## Automatic Resource Management with `Component` + +Inside the `Plugin` class, you have access to several helper methods for registering and automatically cleaning up resources when unloaded. + + +```ts +// Good +class MyPlugin extends Plugin { + onload() { + this.registerDomEvent(window, "resize", onResize); + } +} +``` + +The `registerDomEvent` call automatically removes the listener when your plugin unloads, so you don’t have to track it manually. The same applies to other helper methods: + +### `registerEvent(eventRef)` + +Registers an Obsidian `EventRef` (from `app.vault.on(...)`, etc.). +The event will be automatically detached when unloaded. + +```ts +this.registerEvent(this.app.vault.on("modify", onFileModify, this)); +``` + +### `registerInterval(id)` + +Registers an interval created by `setInterval`. +All registered intervals are automatically cleared on unload. + +```ts +const id = window.setInterval(doSomethingPeriodically, 1000); +this.registerInterval(id); +``` + +### `registerDomEvent(el, event, handler)` + +Registers a DOM event on an element or window. +Automatically removed on unload. + +```ts +this.registerDomEvent(window, "resize", onResize); +``` + +These methods will ensure that any listener you create is always properly released when your plugin or component unloads. + + +## The Lifecycle of Objects + + +A good rule of thumb: +> [!NOTE] +> **Any resource created during `onload` (or later) should be cleaned up in `onunload`.** + +This applies to various types of resources, including but not limited to: +- DOM event listeners +- Obsidian event listeners +- Intervals and timeouts +- Workers, network connections, or WASM instances +- Third-party libraries that need explicit disposal + +Before adding any resource, you should ask yourself: +> [!NOTE] +> **“Who owns this, and how long should it live?”** + +Consider the following scenario, you want to listen for clicks on a button inside a custom view, and keyboard inputs on the entire window. + +```ts +class MyView extends ItemView { + constructor(leaf: WorkspaceLeaf) { + super(leaf); + const button = this.containerEl.appendChild( + createEl("button", { text: "Click me!" }) + ); + // Good + button.addEventListener("click", onButtonClick); + } +} + +class MyPlugin extends Plugin { + onload() { + // Bad + window.addEventListener("keydown", onKeyDown); + } +} +``` + + +You might ask yourself: "_Why is the button listener fine, but the window listener is bad?_". +It comes down to the lifetime of the objects: + +The button listener is fine, because when the view is closed, the button will also be removed from the DOM. The listener disappears with it. +The window listener, however, persists even after your plugin is disabled, so it must be explicitly cleaned up! + + +## Components and the Hierarchy Model + +The `Component` class is central to how Obsidian manages resources. +Every `Component` can: + +- Register cleanup tasks (using `registerEvent`, `registerInterval`, etc.) +- Contain child components +- Be added as a child to a parent component + +When a parent component unloads, all its children are automatically unloaded as well. + +Each component in the tree goes through the following lifecycle methods: + +- `onload()` + - The parent component is loaded + - The component gets loaded explicitly (via `component.load()`) + - When added to a loaded parent (via `parent.addChild(component)`) +- `onunload()`: + - The parent component is unloaded + - The component gets unloaded explicitly (via `component.unload()`) + - When removed from a parent (via `parent.removeChild(component)`) + +This hierarchy allows clean organization and ensures that all subcomponents are properly destroyed. + + +## Passing Components to Other APIs + +Some APIs in Obsidian accept a `Component` parameter to help manage lifecycle. +The most common example is the `MarkdownRenderer.render` method, which requires a `Component` to track the rendering context. + +```ts +MarkdownRenderer.render(app, "## Some Markdown", containerEl, "", COMPONENT); +``` + +The `COMPONENT` parameter allows the `MarkdownRenderer` to register itself as a child component of the provided component. +This means that when the parent component unloads, the renderer will also be cleaned up automatically. + +A common mistake that is often made, is that a `Component` is created just for the purpose of passing it to such APIs, but is never actually loaded or unloaded. + +```ts +import { Component, ItemView, MarkdownRenderer } from 'obsidian'; + +class MyView extends ItemView { + onload() { + // Bad: creating a temporary component that is never loaded/unloaded + const tempComponent = new Component(); + MarkdownRenderer.render(this.app, "## Some Markdown", this.containerEl, "", tempComponent); + } +} +``` + +As we have seen in previous sections, this component is never loaded or unloaded, so the renderer will never be cleaned up! + +Instead, you should pass a component that is loaded and unloaded properly. This can be your own, or the plugin itself. + +```ts +import { ItemView, MarkdownRenderer } from 'obsidian'; + +class MyView extends ItemView { + onload() { + // Good: using the view itself as the component + MarkdownRenderer.render(this.app, "## Some Markdown", this.containerEl, "", this); + } +} +``` + + +## Creating Your Own Components + +You can define your own components by extending the `Component` class. +This is useful when you have a logical unit (such as a widget or view) that manages its own state or resources. + +```ts +import { Component, MarkdownRenderer } from "obsidian"; +import SomeLibrary from "some-library"; + +class MyWidget extends Component { + private widget: HTMLElement = document.createDiv({ cls: "my-widget" }); + private lib: SomeLibrary; + + constructor(private app: App) { + super(); + } + + onload() { + MarkdownRenderer.render(this.app, "## Some Markdown", this.widget, "", this); + document.body.appendChild(this.widget); + + this.lib = new SomeLibrary(); + this.lib.initialize(); + + this.registerDomEvent(window, "resize", () => { + this.lib.resize(window.innerWidth, window.innerHeight); + }); + } + + doSomething() { + this.lib.doSomething(); + } + + onunload() { + // Removes the widget from the DOM and cleans up the library + this.widget.remove(); + this.lib.destroy(); + } +} +``` + +This widget class fully encapsulates its own lifecycle: it sets up everything it needs during `onload`, and cleans up automatically during `onunload`. + + +Now, you can add an instance of your `MyWidget` component (or multiple!) to your plugin (or another component), making it part of the hierarchy. Whenever the parent unloads, the child is unloaded as well. + +```ts +class MyPlugin extends Plugin { + // Creates the widget component, but it is not initialized yet! + private widget: MyWidget = new MyWidget(this.app) + + onload() { + // Add the widget as a child component, which will automatically load it, + // as the parent (the plugin) is already loaded. + this.widget = this.addChild(this.widget); + + // And use it as needed + this.widget.doSomething(); + } + + onunload() { + // The widget will be automatically unloaded with the plugin + } + + removeWidget() { + // Unloads the widget component manually + this.removeChild(this.widget); + } +} +``` + +By structuring your plugin around components, you gain clear ownership of resources, automatic cleanup, and easier debugging of lifecycle issues. + +## Summary + +- Use the `Component` class to manage and clean up resources. +- Register intervals, events, and DOM listeners through helper methods (`registerEvent`, `registerInterval`, `registerDomEvent`). +- Pass a `Component` to APIs like `MarkdownRenderer.render` to ensure proper lifecycle handling. +- Organize your plugin into smaller `Component` subclasses to simplify ownership. +- Remember: if you create a resource, make sure it’s cleaned up by `onunload`. diff --git a/en/Plugins/Guides/Using Components.md b/en/Plugins/Guides/Using Components.md deleted file mode 100644 index 796e17f7..00000000 --- a/en/Plugins/Guides/Using Components.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -permalink: plugins/guides/using-components ---- - -The `Component` class is a useful tool for managing the lifecycle of UI elements and other classes in Obsidian plugins. It provides a consistent way to handle setup and teardown logic, and helps ensure that your plugin cleans up resources correctly when it is unloaded. - -In this guide, we'll explore how to use the `Component` class effectively in your Obsidian plugins. -For more details about the `Component` class, see the [API reference](/Reference/TypeScript+API/Component). - -## Overview - -Under the hood, the `Plugin`, `View`, and `Menu` classes all extend from `Component`. -This means that they share the same lifecycle management patterns for loading, unloading, and managing child components. - -By using `Component` in your own classes, you can: - -- Organize your plugin logic into modular, self-contained components. -- Automatically clean up event listeners, DOM elements, and intervals. -- Avoid memory leaks and dangling references when unloading. -- Manage external or heavy resources, such as libraries, WebAssembly modules or network clients, safely. - -A `Component` is considered **loaded** after its `load()` method has been called, and **unloaded** after its `unload()` method completes. When a component is unloaded, all of its registered resources and child components are automatically cleaned up. - - -## Adding a window resize listener - -Imagine you want to respond to window resize events in your plugin. -A naïve approach might look like this: - -```ts -// Bad -class MyPlugin extends Plugin { - onload() { - window.addEventListener("resize", onResize); - } -} -``` - -However, when the user disables your plugin, the resize listener will continue to run until Obsidian is restarted. -This can lead to unnecessary memory usage or even errors if your handler references unloaded data. - -To fix this, you might manually remove the listener in `onunload`: - -```ts -// Better -class MyPlugin extends Plugin { - onload() { - window.addEventListener("resize", onResize); - } - - onunload() { - window.removeEventListener("resize", onResize); - } -} -``` - -While this works, it is easy to forget or duplicate cleanup code across different parts of your plugin. -Fortunately, since the `Plugin` class extends `Component`, you can use the `registerDomEvent` helper to handle this automatically: - -```ts -// Good -class MyPlugin extends Plugin { - onload() { - this.registerDomEvent(window, "resize", onResize); - } -} -``` - -Whenever your plugin is unloaded, Obsidian will automatically detach the resize listener, this way you don't have to worry about it! - - - -## Handling different types of events - -In addition to DOM events, `Component` provides helper methods for managing other kinds of resources that need cleanup, such as event listeners within Obsidian itself or active intervals. - -```ts -class MyPlugin extends Plugin { - onload() { - // Register an event listener from Obsidian's `Events` system - this.registerEvent(this.app.vault.on("modify", onFileModify, this)); - - // Register an interval (automatically cleared on unload) - const intervalId = setInterval(onIntervalTick, 1000); - this.registerInterval(intervalId); - } -} -``` - -These registration methods ensure that all listeners and intervals are released when your component unloads. -You can use `register()` for generic cleanup callbacks that don't fall into any of these categories. - - -## Defining your own components - -Beyond `Plugin`, `View`, and other built-in types, you can define your own classes that extend `Component`. -This makes it easy to encapsulate behavior and manage resources for reusable parts of your plugin, such as controllers, widgets, or data managers. - -Here's an example that renders some Markdown in a DOM element, initializes an imaginary external library, and handles window resize events: - -```ts -import { Component } from "obsidian"; -import SomeLibrary from "some-lib"; - -class MyWidget extends Component { - private container: HTMLElement; - private lib: SomeLibrary; - - constructor(private app: App) { - super(); - } - - onload() { - // Add an element to the DOM and render Markdown in it - // The MarkdownRenderer takes the current component as its parent, and adds itself as a child component. - this.container = document.createDiv({ cls: "my-widget" }); - MarkdownRenderer.render(this.app, "## Some Markdown", this.container, "", this); - document.body.appendChild(this.container); - - // Initialize an external resource - this.lib = new SomeLibrary(); - this.lib.initialize(); - - // Handle window resize automatically - this.registerDomEvent(window, "resize", () => { - this.lib.resize(window.innerWidth, window.innerHeight); - }); - } - - doSomething() { - this.lib.doSomething(); - } - - onunload() { - // Clean up when the widget is unloaded - this.container.remove(); - this.lib.destroy(); - } -} -``` - -This widget encapsulates its own lifecycle, initializing when loaded, and cleaning up when unloaded. - -Now, you can attach your `MyWidget` component to your plugin or other components, forming a clear hierarchy of resources. -When a parent component is loaded or unloaded, all of its child components are automatically loaded or unloaded too. - -```ts -class MyPlugin extends Plugin { - private widget: MyWidget; - - onload() { - // Add the widget as a child component - this.widget = this.addChild(new MyWidget(this.app)); - - // And use it as needed - this.widget.doSomething(); - } - - onunload() { - // The widget will automatically unload with the plugin - } - - temporarilyUnloadWidget() { - // You can also unload it temporarily if needed - this.widget.unload(); - window.setTimeout(() => this.widget.load(), 5000); - } - - removeWidget() { - // You can also explicitly remove it at any time - this.removeChild(this.widget); - } -} -``` - -## Summary - -Using the `Component` class allows your plugin to safely manage lifecycle-bound resources in a structured way. -By taking advantage of built-in helpers like `registerDomEvent`, `registerEvent`, and `registerInterval`, you can ensure that your plugin unloads cleanly without memory leaks or dangling references. - -When designing your plugin's architecture, consider extending or composing `Component` objects whenever you need lifecycle-aware behavior. -This will help keep your code modular, predictable, and easy to maintain. - From e6ab7d3d2b7f382eb260d1add56930cbed086631 Mon Sep 17 00:00:00 2001 From: Fevol <8179397+fevol@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:18:47 +0100 Subject: [PATCH 3/9] docs: address comments --- en/Plugins/Guides/Manage plugin lifecycle.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 6e272ea9..daac31a6 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -46,7 +46,7 @@ class MyPlugin extends Plugin { } ``` -While this works, it quickly becomes repetitive and error-prone. Fortunately, Obsidian’s `Component` system offers a better solution. +While this works, it quickly becomes repetitive and error-prone. Fortunately, Obsidian's `Component` system offers a better solution. ## Automatic Resource Management with `Component` @@ -63,7 +63,7 @@ class MyPlugin extends Plugin { } ``` -The `registerDomEvent` call automatically removes the listener when your plugin unloads, so you don’t have to track it manually. The same applies to other helper methods: +The `registerDomEvent` call automatically removes the listener when your plugin unloads, so you don't have to track it manually. The same applies to other helper methods: ### `registerEvent(eventRef)` @@ -221,7 +221,7 @@ import { Component, MarkdownRenderer } from "obsidian"; import SomeLibrary from "some-library"; class MyWidget extends Component { - private widget: HTMLElement = document.createDiv({ cls: "my-widget" }); + private widget: HTMLElement = createDiv({ cls: "my-widget" }); private lib: SomeLibrary; constructor(private app: App) { @@ -290,4 +290,4 @@ By structuring your plugin around components, you gain clear ownership of resour - Register intervals, events, and DOM listeners through helper methods (`registerEvent`, `registerInterval`, `registerDomEvent`). - Pass a `Component` to APIs like `MarkdownRenderer.render` to ensure proper lifecycle handling. - Organize your plugin into smaller `Component` subclasses to simplify ownership. -- Remember: if you create a resource, make sure it’s cleaned up by `onunload`. +- Remember: if you create a resource, make sure it's cleaned up by `onunload`. From 6644b51cc42561626a839beeab60fb7c45fd7ea8 Mon Sep 17 00:00:00 2001 From: Fevol <8179397+Fevol@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:35:21 +0100 Subject: [PATCH 4/9] docs: improve lifecycle description for view example --- en/Plugins/Guides/Manage plugin lifecycle.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index daac31a6..0a20c65d 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -23,8 +23,12 @@ A simple (but incorrect implementation!) might look like this: ```ts // Bad class MyPlugin extends Plugin { + onResize() { + // ... + } + onload() { - window.addEventListener("resize", onResize); + window.addEventListener("resize", this.onResize); } } ``` @@ -36,12 +40,13 @@ A more responsible solution would be to manually remove the listener during `onu ```ts // Better class MyPlugin extends Plugin { + // ... onload() { - window.addEventListener("resize", onResize); + window.addEventListener("resize", this.onResize); } onunload() { - window.removeEventListener("resize", onResize); + window.removeEventListener("resize", this.onResize); } } ``` @@ -57,6 +62,7 @@ Inside the `Plugin` class, you have access to several helper methods for registe ```ts // Good class MyPlugin extends Plugin { + // ... onload() { this.registerDomEvent(window, "resize", onResize); } @@ -197,14 +203,15 @@ class MyView extends ItemView { As we have seen in previous sections, this component is never loaded or unloaded, so the renderer will never be cleaned up! -Instead, you should pass a component that is loaded and unloaded properly. This can be your own, or the plugin itself. +Instead, you should pass a component that is loaded and unloaded properly, and should live for just as long - but never longer - as the view. +Luckily, _every_ view is also a `Component`, so we can simply pass the `View` instance itself! ```ts import { ItemView, MarkdownRenderer } from 'obsidian'; class MyView extends ItemView { onload() { - // Good: using the view itself as the component + // Good: using the view itself (`this`) as the component MarkdownRenderer.render(this.app, "## Some Markdown", this.containerEl, "", this); } } From 936a8db50f2b1b3f992c4c95b1e5b1567bc8437f Mon Sep 17 00:00:00 2001 From: Fevol <8179397+fevol@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:14:55 +0100 Subject: [PATCH 5/9] docs: address new comments (use modal for example, remove redundant whitespaces) docs: remove passive voice constructions docs: improve comments for codeblocks --- en/Plugins/Guides/Manage plugin lifecycle.md | 254 ++++++++++++------- 1 file changed, 167 insertions(+), 87 deletions(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 0a20c65d..99b7575b 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -2,29 +2,29 @@ permalink: plugins/guides/lifecycle-management --- -When developing a plugin, you will often create resources such as event listeners, DOM elements, intervals or workers. While setting these up is often straightforward, it is equally important to properly clean them up when your plugin is unloaded. Neglecting this can lead to memory leaks, dangling event handlers, or unexpected behavior. +When developing a plugin, you will often create resources such as event listeners, DOM elements, intervals, and workers. While setting these up is straightforward, it is equally important to properly clean them up when your plugin is unloaded. Without proper cleanup, you'll create memory leaks, orphaned event handlers, and bugs that persists even after your plugin is unloaded. ## What you'll learn After completing this guide, you'll be able to: -- Manage the lifecycle of resources in your plugin. +- Manage resource lifecycles in your plugin. - Use the `Component` class to simplify cleanup and organization. -- Avoid common pitfalls that lead to resource leaks or stale data. +- Avoid common pitfalls that cause resource leaks. - Understand how components and subcomponents interact in Obsidian. ## Overview Imagine your plugin needs to respond to window resize events. -A simple (but incorrect implementation!) might look like this: +Here's a simple (but incorrect!) implementation: ```ts -// Bad +// Bad: the listener is never removed! class MyPlugin extends Plugin { onResize() { - // ... + console.debug("Window resized!"); } onload() { @@ -33,14 +33,17 @@ class MyPlugin extends Plugin { } ``` -This works, but it introduces a problem: when the user disabled your plugin, the resize listener remains active! Only when Obsidian is restarted will it be removed. While this might be a fairly innocuous example, in more complex scenarios, this can lead to errors and memory leaks. +This code works initially, but it introduces a problem: when users disable your plugin, the resize listener stays active! It will continue firing until Obsidian is restarted. In this simple example, the impact might be minor, but in a more complex plugin, this can quickly lead to errors and memory leaks. A more responsible solution would be to manually remove the listener during `onunload`: ```ts -// Better +// Better: but more tedious and error-prone. class MyPlugin extends Plugin { - // ... + onResize() { + console.debug("Window resized!"); + } + onload() { window.addEventListener("resize", this.onResize); } @@ -51,76 +54,76 @@ class MyPlugin extends Plugin { } ``` -While this works, it quickly becomes repetitive and error-prone. Fortunately, Obsidian's `Component` system offers a better solution. +This works, but as your plugin grows, manually tracking every resource you instantiate becomes unwieldy and error-prone. Obsidian's `Component` system provides a better solution. ## Automatic Resource Management with `Component` -Inside the `Plugin` class, you have access to several helper methods for registering and automatically cleaning up resources when unloaded. +The `Plugin` class (which extends `Component`), provides access to several helper methods that automatically clean up resources when your plugin unloads: ```ts -// Good +// Best: the listener is automatically cleaned up! class MyPlugin extends Plugin { - // ... + onResize() { + console.debug("Window resized!"); + } + onload() { - this.registerDomEvent(window, "resize", onResize); + this.registerDomEvent(window, "resize", this.onResize); } } ``` -The `registerDomEvent` call automatically removes the listener when your plugin unloads, so you don't have to track it manually. The same applies to other helper methods: +That's it. When your plugin unloads, the resize listener is automatically removed, without you having to write any additional cleanup code. + +Here are the main helper methods provided by `Component` for resource management: ### `registerEvent(eventRef)` -Registers an Obsidian `EventRef` (from `app.vault.on(...)`, etc.). -The event will be automatically detached when unloaded. +Registers an Obsidian `EventRef` listener (from `app.vault.on(...)`, `app.workspace.on(...)`, etc.) that's automatically detached on unload. ```ts -this.registerEvent(this.app.vault.on("modify", onFileModify, this)); +this.registerEvent( + this.app.vault.on("modify", (file) => { + console.debug("File modified:", file.path); + }) +); ``` ### `registerInterval(id)` -Registers an interval created by `setInterval`. -All registered intervals are automatically cleared on unload. +Registers an interval created by `setInterval`, automatically cleared on unload. ```ts -const id = window.setInterval(doSomethingPeriodically, 1000); +const id = window.setInterval(() => { + console.debug("Periodic task running..."); +}, 1000); this.registerInterval(id); ``` ### `registerDomEvent(el, event, handler)` -Registers a DOM event on an element or window. -Automatically removed on unload. +Registers a DOM event listener, automatically removed on unload. ```ts -this.registerDomEvent(window, "resize", onResize); +this.registerDomEvent(window, "resize", () => { + console.debug("Window resized!"); +}); ``` -These methods will ensure that any listener you create is always properly released when your plugin or component unloads. - +These methods will ensure your resources are always properly released when your plugin or component unloads. -## The Lifecycle of Objects +## Understanding The Lifecycle of Objects -A good rule of thumb: +Before adding any resource to your plugin, ask yourself: > [!NOTE] -> **Any resource created during `onload` (or later) should be cleaned up in `onunload`.** +> **“Who owns this resource, and how long should it live?”** -This applies to various types of resources, including but not limited to: -- DOM event listeners -- Obsidian event listeners -- Intervals and timeouts -- Workers, network connections, or WASM instances -- Third-party libraries that need explicit disposal +Some resources are naturally tied to the lifecycle of its parent, while some will persist long after its parent is gone. -Before adding any resource, you should ask yourself: -> [!NOTE] -> **“Who owns this, and how long should it live?”** - -Consider the following scenario, you want to listen for clicks on a button inside a custom view, and keyboard inputs on the entire window. +Consider the following two examples: ```ts class MyView extends ItemView { @@ -129,119 +132,167 @@ class MyView extends ItemView { const button = this.containerEl.appendChild( createEl("button", { text: "Click me!" }) ); - // Good + // Good: the listener lives and dies with the button element. button.addEventListener("click", onButtonClick); } } -class MyPlugin extends Plugin { +class MyModal extends Modal { + onKeyDown() { + console.debug("Key pressed!"); + } + onload() { - // Bad - window.addEventListener("keydown", onKeyDown); + // Bad: listener outlives the modal! + window.addEventListener("keydown", this.onKeyDown); } } ``` +Why is the button listener acceptable, while the window listener is problematic? + +The button listener is scoped to an element inside the View. Whenever the view closes and is removed from the DOM, the button disappears, and the listener is garbage collected alongside it. + +The `window` listener is attached to the global `window` object, which persists indefinitely. If you don't explicitly remove the listener in `onClose` or `onunload`, it will keep firing forever! + +Here's the corrected modal: -You might ask yourself: "_Why is the button listener fine, but the window listener is bad?_". -It comes down to the lifetime of the objects: +```ts +class MyModal extends Modal { + onKeyDown() { + console.debug("Key pressed!"); + } + + onload() { + window.addEventListener("keydown", this.onKeyDown); + } -The button listener is fine, because when the view is closed, the button will also be removed from the DOM. The listener disappears with it. -The window listener, however, persists even after your plugin is disabled, so it must be explicitly cleaned up! + onClose() { + window.removeEventListener("keydown", this.onKeyDown); + } +} +``` -## Components and the Hierarchy Model +## Resources That Need Explicit Cleanup + +A good rule of thumb: +> [!NOTE] +> **Any resource created during `onload` (or later) should be cleaned up in `onunload`.** + +This applies to various types of resources, including but not limited to: +- **Global event listeners:** attached to `window`, `document`, or any other long-lived object. +- **Obsidian event listeners:** created via `app.vault.on(...)`, `app.workspace.on(...)`, etc. +- **Intervals and timeouts:** created via `setInterval` or `setTimeout`. +- **External connections:** web workers, network connections, WebSocket connections. +- **Third-party libraries**: Anything that requires explicit disposal (e.g. databases, charting libraries, ...) +- **Heavy memory allocations:** large data structures, WASM instances, etc. + + +## The Component Hierarchy Model The `Component` class is central to how Obsidian manages resources. Every `Component` can: -- Register cleanup tasks (using `registerEvent`, `registerInterval`, etc.) +- Register resources for automatic cleanup (using `registerEvent`, `registerInterval`, etc.) - Contain child components -- Be added as a child to a parent component +- Be nested within a parent component When a parent component unloads, all its children are automatically unloaded as well. +This creates a natural hierarchy where resources are cleaned up in the correct order, and ownership is clear. + +### Component Lifecycle -Each component in the tree goes through the following lifecycle methods: +Each component goes through the following lifecycle stages: - `onload()` - - The parent component is loaded - - The component gets loaded explicitly (via `component.load()`) - - When added to a loaded parent (via `parent.addChild(component)`) + - The parent component loads and this component was added as a child + - The component is explicitly loaded (via `component.load()`) + - The component is added to an already-loaded parent (via `parent.addChild(component)`) - `onunload()`: - - The parent component is unloaded - - The component gets unloaded explicitly (via `component.unload()`) - - When removed from a parent (via `parent.removeChild(component)`) + - The parent component unloads + - The component is explicitly unloaded (via `component.unload()`) + - The component is removed from its parent (via `parent.removeChild(component)`) -This hierarchy allows clean organization and ensures that all subcomponents are properly destroyed. +This hierarchy ensures proper teardown: children are created _after_ their parents, and destroyed _before_ their parents, preventing orphaned resources and memory leaks. ## Passing Components to Other APIs -Some APIs in Obsidian accept a `Component` parameter to help manage lifecycle. -The most common example is the `MarkdownRenderer.render` method, which requires a `Component` to track the rendering context. +Some APIs in Obsidian require a `Component` parameter to manage the lifecycle of whatever they create. +The most common example is the `MarkdownRenderer.render` method: ```ts MarkdownRenderer.render(app, "## Some Markdown", containerEl, "", COMPONENT); ``` -The `COMPONENT` parameter allows the `MarkdownRenderer` to register itself as a child component of the provided component. -This means that when the parent component unloads, the renderer will also be cleaned up automatically. +The `COMPONENT` parameter tells the `MarkdownRenderer` which component 'owns' the rendering context. When the parent component unloads, the renderer cleans up automatically. -A common mistake that is often made, is that a `Component` is created just for the purpose of passing it to such APIs, but is never actually loaded or unloaded. +### Common Pitfall: Orphaned Components + +A frequent mistake is creating a `Component`, solely to pass it to an API, without ever unloading it. ```ts import { Component, ItemView, MarkdownRenderer } from 'obsidian'; class MyView extends ItemView { onload() { - // Bad: creating a temporary component that is never loaded/unloaded + // Bad: tempComponent is never unloaded! const tempComponent = new Component(); MarkdownRenderer.render(this.app, "## Some Markdown", this.containerEl, "", tempComponent); + tempComponent.load(); } } ``` -As we have seen in previous sections, this component is never loaded or unloaded, so the renderer will never be cleaned up! +Since the `tempComponent` is never unloaded, the MarkdownRenderer will never clean up its resources. It becomes a memory leak. + +### Solution: Add or use an existing component -Instead, you should pass a component that is loaded and unloaded properly, and should live for just as long - but never longer - as the view. -Luckily, _every_ view is also a `Component`, so we can simply pass the `View` instance itself! +Instead, you should pass a component that has a clearly defined lifecycle, one that will be loaded and unloaded properly. Since _every_ view is a `Component`, we can simply pass the `View` instance itself via `this`: ```ts import { ItemView, MarkdownRenderer } from 'obsidian'; class MyView extends ItemView { onload() { - // Good: using the view itself (`this`) as the component + // Good: the view itself (`this`) manages the renderer's lifecycle. MarkdownRenderer.render(this.app, "## Some Markdown", this.containerEl, "", this); } } ``` +Now, when the view closes, the renderer cleans up automatically. + ## Creating Your Own Components -You can define your own components by extending the `Component` class. -This is useful when you have a logical unit (such as a widget or view) that manages its own state or resources. +You can also create your own reusable components by extending the `Component` class. +This is particularly useful when you have some logical unit (such as a widget or view) that manages its own state or resources. ```ts import { Component, MarkdownRenderer } from "obsidian"; import SomeLibrary from "some-library"; class MyWidget extends Component { - private widget: HTMLElement = createDiv({ cls: "my-widget" }); + private widget: HTMLElement; private lib: SomeLibrary; constructor(private app: App) { super(); + this.widget = createDiv({ cls: "my-widget" }); } onload() { + // Render markdown content MarkdownRenderer.render(this.app, "## Some Markdown", this.widget, "", this); document.body.appendChild(this.widget); + // Initialize some third-party library this.lib = new SomeLibrary(); this.lib.initialize(); + // Register an event listener this.registerDomEvent(window, "resize", () => { this.lib.resize(window.innerWidth, window.innerHeight); }); @@ -259,42 +310,71 @@ class MyWidget extends Component { } ``` -This widget class fully encapsulates its own lifecycle: it sets up everything it needs during `onload`, and cleans up automatically during `onunload`. - +This widget fully encapsulates its lifecycle. Everything it creates during `onload`, is eventually cleaned up by `onunload`. -Now, you can add an instance of your `MyWidget` component (or multiple!) to your plugin (or another component), making it part of the hierarchy. Whenever the parent unloads, the child is unloaded as well. +Once you have created a custom component, you can add an instance (or multiple!) of it to your plugin (or any another component), making it part of the hierarchy: ```ts class MyPlugin extends Plugin { - // Creates the widget component, but it is not initialized yet! - private widget: MyWidget = new MyWidget(this.app) + private widget: MyWidget; + constructor(app: App, manifest: PLuginManifest) { + super(); + + // Creates the widget component as a child, but it is not initialized yet! + // The widget will be loaded once the plugin is fully loaded. + this.widget = this.addChild(new MyWidget(this.app)); + } + onload() { - // Add the widget as a child component, which will automatically load it, - // as the parent (the plugin) is already loaded. - this.widget = this.addChild(this.widget); - - // And use it as needed + // Use the widget as needed. this.widget.doSomething(); } onunload() { - // The widget will be automatically unloaded with the plugin + // The widget automatically unloads when the plugin unloads, no need to do anything here! } removeWidget() { - // Unloads the widget component manually + // You can also manually remove/unload the widget. this.removeChild(this.widget); } } ``` -By structuring your plugin around components, you gain clear ownership of resources, automatic cleanup, and easier debugging of lifecycle issues. +### Componentized Modal Example + +You can also apply the component pattern to the modal example we saw earlier for better resource management: + +```ts +class MyModal extends Modal { + private component: Component; + + public constructor(app: App) { + super(app); + this.component = new Component(); + this.component.load(); + } + + onOpen() { + // Register the keyboard event on the component + this.component.registerDomEvent(window, "keydown", (evt) => { + console.debug("Key pressed:", evt.key); + }); + } + + onClose() { + // Manually unload the component, which removes the listener + this.removeChild(this.component); + } +} +``` + ## Summary - Use the `Component` class to manage and clean up resources. - Register intervals, events, and DOM listeners through helper methods (`registerEvent`, `registerInterval`, `registerDomEvent`). -- Pass a `Component` to APIs like `MarkdownRenderer.render` to ensure proper lifecycle handling. +- Do not pass orphaned `Component` to APIs like `MarkdownRenderer.render`! - Organize your plugin into smaller `Component` subclasses to simplify ownership. -- Remember: if you create a resource, make sure it's cleaned up by `onunload`. +- Remember: if you create a resource, make sure it will be cleaned up by `onunload`. From e78e5ef105feed5af9d7d98fcdd9ff910155c33c Mon Sep 17 00:00:00 2001 From: Fevol <8179397+fevol@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:35:33 +0100 Subject: [PATCH 6/9] docs: change onload to onOpen for modal examples --- en/Plugins/Guides/Manage plugin lifecycle.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 99b7575b..557d9fea 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -142,7 +142,7 @@ class MyModal extends Modal { console.debug("Key pressed!"); } - onload() { + onOpen() { // Bad: listener outlives the modal! window.addEventListener("keydown", this.onKeyDown); } @@ -163,7 +163,7 @@ class MyModal extends Modal { console.debug("Key pressed!"); } - onload() { + onOpen() { window.addEventListener("keydown", this.onKeyDown); } From e679134f354012c976a0b8fa1424b4ce5e3f6369 Mon Sep 17 00:00:00 2001 From: Fevol <8179397+Fevol@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:01:20 +0100 Subject: [PATCH 7/9] docs: reword sentence to avoid repetition Co-authored-by: Liam Cain --- en/Plugins/Guides/Manage plugin lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 557d9fea..81c9ab69 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -54,7 +54,7 @@ class MyPlugin extends Plugin { } ``` -This works, but as your plugin grows, manually tracking every resource you instantiate becomes unwieldy and error-prone. Obsidian's `Component` system provides a better solution. +As your plugin grows in complexity, manually tracking every resource you instantiate can become unwieldy and error-prone. Obsidian's `Component` system provides a better solution. ## Automatic Resource Management with `Component` From 25d4b20686c303d3077f76d6c407207fc48fdb1d Mon Sep 17 00:00:00 2001 From: Fevol <8179397+Fevol@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:05:52 +0100 Subject: [PATCH 8/9] docs: explicitly mention renderer being loaded in when added --- en/Plugins/Guides/Manage plugin lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 81c9ab69..18f45456 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -262,7 +262,7 @@ class MyView extends ItemView { } ``` -Now, when the view closes, the renderer cleans up automatically. +Now, when the view is loaded, the Markdown renderer loads alongside it; likewise, when the view closes, it cleans the renderer automatically. ## Creating Your Own Components From f796dc92faef6dd3bcfdc5f2cb16080a5d576b87 Mon Sep 17 00:00:00 2001 From: Fevol <8179397+Fevol@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:58:14 +0100 Subject: [PATCH 9/9] docs: improve description of usage of existing components --- en/Plugins/Guides/Manage plugin lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/en/Plugins/Guides/Manage plugin lifecycle.md b/en/Plugins/Guides/Manage plugin lifecycle.md index 18f45456..2eda8fc7 100644 --- a/en/Plugins/Guides/Manage plugin lifecycle.md +++ b/en/Plugins/Guides/Manage plugin lifecycle.md @@ -249,7 +249,7 @@ Since the `tempComponent` is never unloaded, the MarkdownRenderer will never cle ### Solution: Add or use an existing component -Instead, you should pass a component that has a clearly defined lifecycle, one that will be loaded and unloaded properly. Since _every_ view is a `Component`, we can simply pass the `View` instance itself via `this`: +Instead, you should pass a component that has a clearly defined lifecycle, one that will be loaded and unloaded at an appropriate time. Both `Plugin` and `View` are components; however, `View` is a more appropriate choice since its lifespan matches with the `MarkdownRenderer`. We can simply pass the `View` instance itself via `this`: ```ts import { ItemView, MarkdownRenderer } from 'obsidian';