SDKPackagesRenderer Plugin

Renderer Plugin SDK

The @sdk/renderer-plugin SDK provides the basis for the asset renderer, and the building blocks to create your own plugable render system.

Installation

To install this package, you need to have access to the private @sdk npm scope.

To install the library, run the following command in your project directory:

npm install @sdk/renderer-plugin

Renderer Plugin System

The Renderer Plugin System is a modular and extensible framework that allows developers to add pluggable features in a structured and maintainable way. A Renderer Plugin System promotes code organization, separation of concerns, and team collaboration by providing a unified API for plugin development.


Benefits of the Plugin System

  1. Code Organization: Plugins encapsulate specific functionality, making the codebase modular and easier to maintain.
  2. Separation of Concerns: Each plugin handles a distinct feature, reducing interdependencies and improving scalability.
  3. Team Collaboration: Different teams can develop plugins independently, as long as they adhere to the unified API.
  4. Extensibility: New features can be added without modifying the core rendering logic.
  5. Isolation: Plugins are isolated from each other, ensuring that changes in one plugin do not inadvertently affect others.

Setting Up a Renderer Plugin System

A Renderer Plugin System consists of three working parts: a plugin manager, a context, and a collection of plugins. This section describes how to set up the manager with its context, the next section details how to implement plugins that can be used within this system and make use of the plugin context.

1. Create a RendererPluginManager

The RendererPluginManager is the central hub for managing plugins. Upon construction it takes a ‘context’, which is passed to every plugin’s lifecycle hooks.

const rendererPluginContext = {
  getLogger(): Logger;
}
 
const rendererPluginManager = new RendererPluginManager(rendererPluginContext);

2. Register Plugins

Plugins can be registered with the RendererPluginManager using the registerPlugin method.

rendererPluginManager.registerPlugin(new MyPlugin());

3. Initialize Plugins

After registering plugins, call the initialize method to initialize the plugins.

rendererPluginManager.initialize();

3a. Retrieve Plugins

Plugins can be retrieved using either getPlugin or getPluginSafe:

  • getPlugin: Retrieves a plugin by its constructor. Throws an error (TypeError) if the plugin is not registered. This is useful when your code expects the plugin to be registered.

    const myPlugin = rendererPluginManager.getPlugin(MyPlugin);
  • getPluginSafe: Retrieves a plugin by its constructor. Returns undefined if the plugin is not registered.

    const myPlugin = rendererPluginManager.getPluginSafe(MyPlugin);
    if (myPlugin) {
      // Use the plugin
    }

4. Teardown Plugins

When the renderer is no longer needed, call the teardown method to clean up resources and remove event listeners.

rendererPluginManager.teardown();

Creating a Plugin

Plugins must implement the RendererPlugin interface. A plugin inside a basic Renderer Plugin System has a very straightforward lifecycle:

  1. hookRegister: Called when the plugin is registered. Determines whether the plugin should be enabled.
  2. hookInitialize: Called when the renderer is manually initialized. Layer information is not yet available.
  3. hookTeardown: Called when the plugin is torn down. Use this to clean up resources.

The context that is passed to the renderer plugin manager upon construction of the object, is also passed as argument to each of these hooks. For example:

interface RendererPluginContext {
  getLogger(): Logger;
}
 
class MyPlugin implements RendererPlugin {
  hookInitialize(context: RendererPluginContext) {
    context.getLogger().log('MyPlugin got initialized');
  }
}

Plugin implementations can make use of the mixin system for common functionality like event handling and automatic resource management.

Using Plugins in a React Context

Plugins can be used in a React context through the use of various hooks. In order to make a React component tree aware of a renderer plugin system, you need to make use of the React RendererContext:

let rendererPluginManager: RendererPluginManager; // comes from somewhere
 
const MyApp = () => {
  return (
    <RendererContext.Provider value={rendererPluginManager}>
      {/* you can use the hooks described below in here now */}
    </RendererContext.Provider>
  );
};

You can retrieve the manager with React’s useContext hook, although if you want to interact with plugins it’s recommended to instead use the hooks described in the following sections.

useRendererPlugin

This hook can be called anywhere to get a plugin instance, if it’s registered, of the constructor that is passed to the hook. Note that your component will have to check whether the result of the hook is null or an actual plugin instance:

const myPlugin = useRendererPlugin(MyPlugin);
if (myPlugin !== null) {
  // do stuff with myPlugin
}

useRendererPluginStore/useRendererPluginStoreLazy

These hooks can be used when your plugin implements the Stateful interface (which comes for free if you use the Stateful mixin). If you have a non-null plugin instance, you can use useRendererPluginStore to access (parts of) the underlying zustand store using a familiar zustand signature.

const MyComponent = ({ myPlugin }: { myPlugin: MyPlugin }) => {
  const myValue = useRendererPluginStore(myPlugin, state => state.myValue);
  return <Text>My value is {myValue}</Text>;
};

If you have a nullable plugin instance (for example, if you want to use the result of useRendererPlugin directly), you can make use of useRendererPluginStoreLazy that in itself will return a nullable object.

const MyComponent = () => {
  const myPlugin = useRendererPlugin(MyPlugin);
  const myValue = useRendererPluginStoreLazy(myPlugin, state => state.myValue);
  return myValue ? <Text>My value is {myValue}</Text> : <Text>No value detected!</Text>;
};

Best Practices

No Direct Plugin-to-plugin Communication

Plugins are isolated from each other and should not directly reference one another. This is to promote decoupling of features, which makes it both easier to test these features and to more easily compose these features in our various products.

In order to let various plugins communicate, you should make use of an event-based system. It’s recommended to use the Evented mixin to add an event system to a plugin.

It’s recommended to do the “wiring” of plugins as close to the composition root as possible, preferably in the same place where the renderer plugin system is created and the plugins are instantiated and registered.

Avoid Using The Renderer Plugin Manager Directly

Avoid manipulating the RendererPluginManager directly in React components. Instead, make use of the hooks described in the previous section, and decouple your components by passing individual plugins as needed. This ensures that components only have access to the plugins they require and reduces the risk of coupling.

Don’t Leak Plugin Internals

A core principle of good software design is encapsulation: keeping internal details hidden and providing a clean, minimal API for external use. When a plugin reveals its internal helper objects or logic (for example, by returning them directly from a getSupport() method), it creates tight coupling between the plugin and the code that uses it. Instead, it is better to expose only the methods that callers truly need—methods that are part of the plugin’s public interface. This high-level API ensures that external code depends on stable, well-defined functionality, while the plugin remains free to evolve or refactor its internals as needed.

// ⛔️ Don't
myPlugin.getSupport().doTheThing();
 
// ✅ Do
myPlugin.doTheThing();

Always expose a clear and direct method that accomplishes the desired action, rather than returning internal components.

Mixins

In order to quickly build a plugin, you can make use of the various mixins that this package exposes. To add functionality to a plugin, you must use of the Mix compositor function, pick one or more of the mixins described in this section, and then extend your plugin class from that.

For example, in order to add events and automatic resource management to a plugin, you would do:

type MyPluginEvents = {
  onPluginEvent: { foo: string };
};
 
class MyPlugin extends mix(Evented<MyPluginEvents>(), DisposableManaged) {}

DisposableManaged

With this mixin, you can add Disposable objects to an internally managed set through addDisposable. These disposables are then automatically cleaned up when hookTeardown is called on the plugin.

A Disposable is actually a simple interface of objects that expose a dispose method; there are also two convenience methods to quickly turn common ‘resources’ (such as RIA handles or store subscriptions) into one such a Disposable, e.g:

class MyPlugin extends mix(DisposableManaged) {
  hookInitialize() {
    this.addDisposable(
      Disposable.fromCallback(
        // for example, subscribing to a zustand store
        store
          .subscribe
          // etc.
          ()
      )
    );
  }
}

Evented

This mixin adds an event system to your plugin.

type MyPluginEvents = {
  onPluginEvent: { foo: string };
};
 
class MyPlugin extends mix(Evented<MyPluginEvents>(), DisposableManaged) {
  change() {
    this.dispatchEvent('onPluginEvent', { foo: 'bar' });
  }
}
 
// later
let myPlugin: MyPlugin;
myPlugin.addEventListener('onPluginEvent', ({ foo }) => {
  // do some foo stuff
});

Stateful

This mixin unlocks interaction with the useRendererPluginStore and useRendererPluginStoreLazy. It also makes sure that the store is reset to its initial state upon construction of the plugin that uses it.

class MyPlugin extends mix(Stateful(store)) {}
 
// in a component
const myPluginState = useRendererPluginStoreLazy(myPlugin)(state => /* select the state */)

Note that a typical use case throughout our code base is to have an ‘internal’ store API that can only be used in the plugin itself, and a public facing, more restrictive API that can only be used outside of the plugin. In order to facilitate this pattern, a helper function hidePrivateProperties can be used:

class MyPlugin extends mix(Stateful(hidePrivateProperties(store))) {}

When this function is used on the store, the public API of the store will be that of the store passed, but any properties on the store object that are prefixed with an underscore will be ‘removed’ (as in, they will still be present on the object, but no longer exist according to the type of the object).

For backwards compatibility, you can still use the previous method of implementing this functionality, i.e. by implementing the StatefulRendererPlugin interface. This will also still work with the useRendererPluginStore and useRendererPluginStoreLazy hooks.