@pinosandro/react-plugin-system
v1.1.0
Published
A lightweight and flexible plugin system for React applications
Maintainers
Readme
🔌 React Plugin System
A plugin-based approach focuses on features and promotes a more intuitive, clear, and maintainable form of modularity. This plugin system for React applications takes inspiration from Spotify’s Backstage, a full-fledged framework that I use daily at work.
The goal of this library is to provide a lightweight, flexible, and minimal plugin system for React, with no external constraints or dependencies beyond React itself.
Whether you use it in a traditional project, a monorepo, or with separate repositories for each plugin, there’s no wrong way to use this library.
Installation
In the root of your React project:
npm install @pinosandro/react-plugin-systemUsage
The react-plugin-system allows you to extend your application by creating and registering plugins. Each plugin provides its own API that can be accessed anywhere in your app using the usePluginApi hook.
Create a Plugin
It is possible to create a new plugin using the Plugin class. Each plugin should have a unique ID, following the naming convention author-pluginname. The createApiClient function defines the plugin's API.
import { Plugin } from '@pinosandro/react-plugin-system';
// naming convention: author-pluginname
export const EXAMPLE_PLUGIN_ID = 'author-example';
export const examplePlugin = new Plugin({
id: EXAMPLE_PLUGIN_ID,
createApiClient() {
return {
printHello() {
console.log('Hello from examplePlugin API Client!');
},
};
},
});Register a plugin
The createPluginApp function wraps your App within the plugin context, allowing the APIs of all registered plugins to be accessed anywhere in your application.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { createPluginApp } from '@pinosandro/react-plugin-system';
import { App } from './App.jsx';
import { examplePlugin } from './plugins';
const PluginApp = createPluginApp({
plugins: [examplePlugin],
App,
});
createRoot(document.getElementById('root')).render(
<StrictMode>
<PluginApp />
</StrictMode>,
);Consume the Plugin Api
You can access the registered plugin's API by passing its unique ID to the usePluginApi hook.
import { usePluginApi } from '@pinosandro/react-plugin-system';
import { EXAMPLE_PLUGIN_ID } from './plugins';
export function App() {
const exampleApi = usePluginApi(EXAMPLE_PLUGIN_ID);
exampleApi.printHello();
return <div>App</div>;
}Plugin Dependencies
A plugin can depend on other plugins if it uses them internally. Here’s an example of how to define such a plugin:
import { Plugin } from '@pinosandro/react-plugin-system';
import { EXAMPLE_PLUGIN_ID } from '../example.js/example';
export const DEPS_EXAMPLE_PLUGIN_ID = 'author-depsexample';
export const depsExamplePlugin = new Plugin({
id: DEPS_EXAMPLE_PLUGIN_ID,
dependencies: {
exampleApi: EXAMPLE_PLUGIN_ID,
},
createApiClient({ exampleApi }) {
return {
anotherHelloMethod() {
console.log('Hello from depsExamplePlugin API Client!');
exampleApi.printHello();
},
};
},
});Once a plugin is created, it must be registered in the PluginApp after its dependencies; otherwise, an error will be thrown.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { createPluginApp } from '@pinosandro/react-plugin-system';
import { App } from './App.jsx';
import { depsExamplePlugin, examplePlugin } from './plugins';
const PluginApp = createPluginApp({
plugins: [examplePlugin, depsExamplePlugin],
App,
});
createRoot(document.getElementById('root')).render(
<StrictMode>
<PluginApp />
</StrictMode>,
);Plugin Provider
It is possible to add a React context through the provider property. This extends, simplifies, and speeds up the integration of plugins that require context. The provider automatically wraps the application when the plugin is registered in the app.
If multiple plugins define a provider, each provider will wrap the previous one, following the order in which the plugins are registered.
import { Plugin } from '@pinosandro/react-plugin-system';
import { createApiClient } from './plugin.client';
export const FEEDBACK_PLUGIN_ID = 'pinosandro-feedback';
export const feedbackPlugin = new Plugin({
id: FEEDBACK_PLUGIN_ID,
createApiClient,
provider: FeedbackProvider,
});Types
The library provides native TypeScript support, but if you're using JavaScript, it's recommended to add declaration files .d.ts to benefit from autocompletion and a better developer experience.
JavaScript (.d.ts)
For each plugin, create a .d.ts file following this example:
import { DEPS_EXAMPLE_PLUGIN_ID } from './depsExample';
interface DepsExamplePluginApi {
anotherHelloMethod(): void;
}
declare module '@pinosandro/react-plugin-system' {
interface PluginApiStore {
[DEPS_EXAMPLE_PLUGIN_ID]: DepsExamplePluginApi;
}
}Once the declaration file is created, you can reference it directly in your JavaScript plugin. This ensures that your plugin has full access to the shared type definitions and API contracts defined above.
// @ts-check
/// <reference path="./index.d.ts" />
import { Plugin } from '@pinosandro/react-plugin-system';
import { EXAMPLE_PLUGIN_ID } from '../example.js/example';
export const DEPS_EXAMPLE_PLUGIN_ID = 'author-depsexample';
export const depsExamplePlugin = new Plugin({
id: DEPS_EXAMPLE_PLUGIN_ID,
dependencies: {
exampleApi: EXAMPLE_PLUGIN_ID,
},
createApiClient({ exampleApi }) {
return {
anotherHelloMethod() {
console.log('Hello from depsExamplePlugin API Client!');
exampleApi.printHello();
},
};
},
});Typescript
If you're using TypeScript, the plugin declaration should be like this:
import { Plugin } from '@pinosandro/react-plugin-system';
import { EXAMPLE_PLUGIN_ID } from '../example/example';
export const DEPS_EXAMPLE_PLUGIN_ID = 'author-depsexample';
interface DepsExamplePluginApi {
anotherHelloMethod(): void;
}
declare module '@pinosandro/react-plugin-system' {
interface PluginApiStore {
[DEPS_EXAMPLE_PLUGIN_ID]: DepsExamplePluginApi;
}
}
export const depsExamplePlugin = new Plugin({
id: DEPS_EXAMPLE_PLUGIN_ID,
dependencies: {
exampleApi: EXAMPLE_PLUGIN_ID,
},
createApiClient({ exampleApi }) {
return {
anotherHelloMethod() {
console.log('Hello from depsExamplePlugin API Client!');
exampleApi.printHello();
},
};
},
});End Notes
The plugin API client is fully customizable to fit your specific needs. It can include not only utility functions but also, for example, React components that can be accessed via usePluginApi.
However, this approach is usually only necessary for special cases. In most scenarios, it’s sufficient to define plugin-related components within the plugin’s folder structure.
License
Licensed under MIT.
