Documentation
UI Plugins

UI Plugins

The Cosmos UI plugin system consists of an intricate web of "slots" and "plugs" that weave together with minimal knowledge of each other.

Though it will take some time to get familiar with the subtleties of the Cosmos UI plugin architecture, its core principles are simple and so is creating your first “Hello World”.

💡

The underlying react-plugin library isn't published yet due to time constraints but will be open sourced as a separate project in the future.

Boilerplate

The ui field in cosmos.plugin.json points to a module like this:

import React from 'react';
import { createPlugin } from 'react-plugin';
 
const plugin = createPlugin({ name: 'magicPlugin' });
 
// We're plugging a React component into an existing slot called "coolSlot"
plugin.plug('coolSlot', () => {
  return <div>This is magic.</div>;
});
 
plugin.register();

Plugin API

Slots and plugs make up for the render composition but there's more to UI plugins. To function as standalone UI abstractions that can interact in meaningful ways, UI plugins can also have individual configuration, private states, public methods and event handlers.

createPlugin

These are the arguments supported when creating a plugin:

ArgumentDescription
nameUI plugin identifier.
defaultConfigOptional plain object config. Set via Cosmos config under ui.{pluginName} and read privately via PluginContext.getConfig.
initialStateOptional plain object state. Accessed privately via PluginContext.getState and PluginContext.setState.
methodsOptional method handlers called by other plugins via PluginContext.getMethodsOf.

Once created, the plugin API allows registering UI plugs, as well as onLoad and event handlers.

Plugin.plug

Plug a React component into a <Slot>:

plugin.plug('slotName', () => {
  return <MyComponent />;
});

Plugs get access to the PluginContext:

plugin.plug('slotName', ({ pluginContext }) => {
  return <MyComponent state={pluginContext.getState()} />;
});

Plugs can receive slot props, which allows slots to parameterize their plugs:

plugin.plug('slotName', ({ slotProps }) => {
  return <MyComponent name={slotProps.name} />;
});

Somewhere in another plugin...

<Slot name="slotName" slotProps={{ name: 'Sara' }} />

Plugs can receive children from their slot. This allows plugs to act as decorators:

plugin.plug('slotName', ({ children }) => {
  return <MyDecorator>{children}</MyDecorator>;
});

Additionally, when multiple plugs are registered for the same slot they can compose each other. The first plug will be children for the second plug, the first plug wrapped in the second will be children for the third plug, and so on.

You can also choose not to render children in a plug, thereby ignoring the slot's children and replacing all previous plugs.

Plugin.namedPlug

Plug a React component into an <ArraySlot>:

plugin.namedPlug('slotName', 'plugName', () => {
  return <MyComponent />;
});

Contrary to Plugin.plug where a plug decorates or replaces previous plugs, a named plug is appended to a list for the slot to render. Multiple named plugs are allowed for the same slot and all will be rendered independently.

Named plugs are rendered in the order they are registered, or based on the optional plugOrder prop of the ArraySlot component.

Plugin.onLoad

Register a handler that gets called once when the Cosmos UI loads, or when the plugin is enabled. If the handler returns a callback it will be called when the plugin is disabled or uninstalled.

plugin.onLoad(pluginContext => {
  // Add DOM event or fetch external data
  return () => {
    // Optional: Clean up and unsubscribe from stuff
  };
});

Plugin.on

Register handlers for events of other plugins.

on('otherPlugin', {
  eventName(pluginContext, arg1, arg2) {
    // React to event from other plugin
  },
});

Plugin.register

Register the plugin in the global store.

PluginContext

The plugin context API allows you to interact with private plugin data (config, state) as well as other plugins (emit events, call methods). All plugin handlers receive the plugin context as the first argument.

PluginContext.getConfig

Read own (private) plugin config.

pluginContext.getConfig();

PluginContext.getState

Read own (private) plugin state.

pluginContext.getState();

PluginContext.setState

Change own (private) plugin state.

pluginContext.setState({ enabled: true });
 
// Or using a state updater callback
pluginContext.setState(prevState => ({
  ...prevState,
  enabled: !prevState.enabled,
}));

PluginContext.getMethodsOf

Get public methods of other plugins.

const otherPlugin = pluginContext.getMethodsOf('otherPlugin');
otherPlugin.doSomething('withThis');
 
// It's also possible for plugin methods to return something
const value = otherPlugin.getSomething();

PluginContext.emit

Emit event to listeners registered by other plugins via Plugin.on.

pluginContext.emit('magicEvent', 'arg1', 'arg2');

Slots

Slots and plugs are reminiscent of the chicken or egg conundrum. In this case, however, the slot definitely came first. Without a root slot nothing gets rendered in the Cosmos UI.

Each plugin can define new slots by rendering Slot and ArraySlot components.

<Slot>

Render a slot that accepts a React component plug.

PropDescription
nameUnique slot name.
childrenOptional children passed to plugs.
slotPropsOptional plain object passed to plugs.
<Slot name="coolSlot" />

Slots can pass children to plugs, as well as data through the slotProps prop:

<Slot name="coolSlot" slotProps={{ cool: true, level: 100 }}>
  <p>This is cool.</p>
</Slot>

<ArraySlot>

Render a slot that accepts a list of React component plugs.

PropDescription
nameUnique slot name.
slotPropsOptional plain object passed to plugs.
plugOrderOptional list of plug names to enforce a sort order.
<ArraySlot name="coolSlot" />

The plug order can be enforced using the plugOrder prop:

<ArraySlot name="coolSlot" plugOrder={['plug1', 'plug2', 'plug3']} />

Built-in Plugins

The Cosmos UI is built entirely using this plugin API. It literally starts like this:

const root = createRoot(container);
root.render(<Slot name="root" />);

You can browse the complete list of built-in UI plugins at packages/react-cosmos-ui/src/plugins (opens in a new tab).

Here's a few examples of existing slots:

TypeSlot nameDescriptionPlug examples
ArraySlotrendererActionRenderer-related buttons.Open fixture source. Go full screen. Toggle responsive preview.
ArraySlotnavRowLeft-hand nav panel widgets.Fixture search. Fixture bookmarks. Fixture tree view.
ArrayrendererPreviewFixture preview placeholder.Iframe renderer for web, status message for React Native.

TypeScript

Like everything else in Cosmos, the UI plugin system is built with TypeScript. All plugin APIs are type-safe and use generics where needed.

The plugin system is built around something called a "spec". The spec is a static interface that allows plugins to interact with each other safely without having to be part of the same codebase.

If you browse the built-in plugins (opens in a new tab) you'll find a spec.ts file inside each plugin. This is an example:

export type RouterSpec = {
  name: 'router';
  state: {
    urlParams: PlaygroundParams;
  };
  methods: {
    getSelectedFixtureId(): null | FixtureId;
    selectFixture(fixtureId: FixtureId): void;
    unselectFixture(): void;
  };
  events: {
    fixtureSelect(fixtureId: FixtureId): void;
    fixtureReselect(fixtureId: FixtureId): void;
    fixtureUnselect(): void;
  };
};

Bundling

Because UI plugins are injected at run-time into the pre-bundled Cosmos UI, they must be bundled separately. This means you'll need to use a bundler like Webpack or Rollup to create a single JS file that Cosmos can load directly in the browser. For the same reason, you need to compile your JSX and TypeScript code to plain JS.

When building your UI plugin make sure it doesn't bundle react or react-plugin inside it. The Cosmos UI plugin system only works when plugins tap into the global React and ReactPlugin instances. The easiest way achive this is by using Webpack externals (opens in a new tab):

externals: {
  'react': 'React',
  'react-dom': 'ReactDom',
  'react-plugin': 'ReactPlugin'
}

See the Boolean input plugin webpack config (opens in a new tab) for a complete example.

For a Vite equivalent for Webpack externals see vite-plugin-externals (opens in a new tab).

💡

In the future we might use import maps (opens in a new tab) to remove the need for a bundler to author UI plugins. ESM support is tracked here.

What Will You Create?

All this might seem intimidating but I encourage you to try it out. Create a blank Cosmos plugin and start hacking. Add something you find useful. Make Cosmos your own.