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-pluginRenderer 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
- Code Organization: Plugins encapsulate specific functionality, making the codebase modular and easier to maintain.
- Separation of Concerns: Each plugin handles a distinct feature, reducing interdependencies and improving scalability.
- Team Collaboration: Different teams can develop plugins independently, as long as they adhere to the unified API.
- Extensibility: New features can be added without modifying the core rendering logic.
- 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. Returnsundefinedif 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:
hookRegister: Called when the plugin is registered. Determines whether the plugin should be enabled.hookInitialize: Called when the renderer is manually initialized. Layer information is not yet available.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.