This pattern focuses on implementing reusable widgets in React that are exposed via an imperative API. Using useImperativeHandle,
React's internal components can be controlled directly from the outside.
This approach is ideal when a component needs to be managed outside React's normal flow, such as in applications integrating third-party systems or components with customized lifecycles.
In this case, we will document a general pattern for externally controllable widgets, using a simple example that demonstrates how to implement this functionality with an injectable key.
-
Singleton Definition:
- Ensures only one instance of the widget exists.
- Allows the creation, rendering, and disposal of the widget through a clear API.
- Supports injecting a custom key to avoid conflicts in the global space.
-
React Component:
- Implements the main functionality of the widget, exposing its API through
useImperativeHandle.
- Implements the main functionality of the widget, exposing its API through
-
Custom Hook (optional):
- Encapsulates logic and state, separating business logic from presentation.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import SimpleWidget from './simple-widget';
const createImperativeWidget = (globalKey = '__WidgetInstance') => {
let instance = window[globalKey] || null;
function createInstance(containerElement) {
let ref = { current: null };
let rootInstance = createRoot(containerElement);
function render() {
if (!ref.current) {
ref = React.createRef();
rootInstance.render(<SimpleWidget ref={ref} />);
}
}
function dispose() {
rootInstance.unmount();
instance = null;
window[globalKey] = null;
}
return {
application: () => ref.current,
render,
dispose
};
}
return {
getInstance: (containerElement) => {
if (!instance && containerElement) {
instance = createInstance(containerElement);
window[globalKey] = instance;
}
return instance;
}
};
};
export default createImperativeWidget;import * as React from 'react';
function SimpleWidget(_, ref) {
const [count, setCount] = React.useState(0);
React.useImperativeHandle(ref, () => ({
increment: () => setCount((prev) => prev + 1),
decrement: () => setCount((prev) => prev - 1),
reset: () => setCount(0)
}));
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
</div>
);
}
export default React.forwardRef(SimpleWidget);import createImperativeWidget from './index';
// Create a widget instance with a custom global key
const WidgetFactory = createImperativeWidget('__CustomWidgetKey');
// Render the widget
const container = document.getElementById('widget-container');
const widget = WidgetFactory.getInstance(container);
widget.render();
// Use exposed methods
widget.application().increment();
widget.application().decrement();
widget.application().reset();- Flexibility: Key injection allows the widget to integrate into global contexts without collisions.
- Reusability: This pattern is reusable for different widgets and use cases.
- Simplicity: Maintains an intuitive API, easy to use even outside the React context.