Skip to content

Commit d8ce60e

Browse files
authored
chore(app-registry): add useActivate hook to activate plugins without rendering (#5230)
1 parent 02a9ee1 commit d8ce60e

File tree

3 files changed

+134
-68
lines changed

3 files changed

+134
-68
lines changed

packages/compass-collection/src/components/collection-tab-provider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import React, { useContext, useRef } from 'react';
22
import type { CollectionTabPluginMetadata } from '../modules/collection-tab';
3+
import type { HadronPluginComponent } from 'hadron-app-registry';
34

45
export interface CollectionTabPlugin {
56
name: string;
6-
component: React.ComponentType<CollectionTabPluginMetadata>;
7+
component: React.ComponentType<CollectionTabPluginMetadata> &
8+
Partial<
9+
Pick<
10+
HadronPluginComponent<CollectionTabPluginMetadata, any>,
11+
'useActivate'
12+
>
13+
>;
714
}
815

916
const CollectionTabsContext = React.createContext<CollectionTabPlugin[]>([]);

packages/compass-collection/src/components/collection-tab.tsx

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const collectionModalContainerStyles = css({
5858

5959
type CollectionTabProps = CollectionTabOptions & {
6060
currentTab: string;
61-
collectionMetadata: CollectionMetadata | null;
61+
collectionMetadata: CollectionMetadata;
6262
renderScopedModals(props: CollectionTabOptions): React.ReactElement[];
6363
// TODO(COMPASS-7405): Remove usage of `renderTabs` for Query.QueryBar role
6464
renderTabs(
@@ -67,7 +67,9 @@ type CollectionTabProps = CollectionTabOptions & {
6767
onTabClick(name: string): void;
6868
};
6969

70-
const CollectionTab: React.FunctionComponent<CollectionTabProps> = ({
70+
const CollectionTabWithMetadata: React.FunctionComponent<
71+
CollectionTabProps
72+
> = ({
7173
namespace,
7274
currentTab,
7375
initialAggregation,
@@ -91,11 +93,8 @@ const CollectionTab: React.FunctionComponent<CollectionTabProps> = ({
9193
});
9294
}
9395
}, [currentTab]);
94-
const pluginTabs = useCollectionTabPlugins();
9596

96-
if (collectionMetadata === null) {
97-
return null;
98-
}
97+
const pluginTabs = useCollectionTabPlugins();
9998

10099
const tabsProps = {
101100
namespace,
@@ -106,20 +105,26 @@ const CollectionTab: React.FunctionComponent<CollectionTabProps> = ({
106105
editViewName,
107106
};
108107
renderTabs(tabsProps); // TODO(COMPASS-7405): Remove usage for Query.QueryBar role
109-
const tabs = pluginTabs.map(({ name, component: Component }) => ({
110-
name,
111-
component: (
112-
<Component
113-
{...collectionMetadata}
114-
namespace={namespace}
115-
aggregation={initialAggregation}
116-
pipeline={initialPipeline}
117-
pipelineText={initialPipelineText}
118-
query={initialQuery}
119-
editViewName={editViewName}
120-
/>
121-
),
122-
}));
108+
const tabs = pluginTabs.map(({ name, component: Component }) => {
109+
const pluginProps = {
110+
...collectionMetadata,
111+
namespace: namespace,
112+
aggregation: initialAggregation,
113+
pipeline: initialPipeline,
114+
pipelineText: initialPipelineText,
115+
query: initialQuery,
116+
editViewName: editViewName,
117+
};
118+
119+
// `pluginTabs` never change in runtime so it's safe to call the hook here
120+
// eslint-disable-next-line react-hooks/rules-of-hooks
121+
Component.useActivate?.(pluginProps);
122+
123+
return {
124+
name,
125+
component: <Component {...pluginProps} />,
126+
};
127+
});
123128
const activeTabIndex = tabs.findIndex((tab) => tab.name === currentTab);
124129

125130
return (
@@ -165,6 +170,24 @@ const CollectionTab: React.FunctionComponent<CollectionTabProps> = ({
165170
);
166171
};
167172

173+
const CollectionTab = ({
174+
collectionMetadata,
175+
...props
176+
}: Omit<CollectionTabProps, 'collectionMetadata'> & {
177+
collectionMetadata: CollectionMetadata | null;
178+
}) => {
179+
if (!collectionMetadata) {
180+
return null;
181+
}
182+
183+
return (
184+
<CollectionTabWithMetadata
185+
collectionMetadata={collectionMetadata}
186+
{...props}
187+
></CollectionTabWithMetadata>
188+
);
189+
};
190+
168191
const ConnectedCollectionTab = connect(
169192
(state: CollectionState) => {
170193
return {

packages/hadron-app-registry/src/register-plugin.tsx

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,90 @@ const useMockOption = <T extends keyof MockOptions>(key: T): MockOptions[T] => {
144144
return useContext(MockOptionsContext)[key];
145145
};
146146

147+
function useHadronPluginActivate<T, S extends Record<string, () => unknown>>(
148+
config: HadronPluginConfig<T, S>,
149+
services: S | undefined,
150+
props: T
151+
) {
152+
const registryName = `${config.name}.Plugin`;
153+
const isMockedEnvironment = useMockOption('mockedEnvironment');
154+
const mockServices = useMockOption('mockServices');
155+
156+
const globalAppRegistry = useGlobalAppRegistry();
157+
const localAppRegistry = useLocalAppRegistry();
158+
159+
const serviceImpls = Object.fromEntries(
160+
Object.keys({
161+
...(isMockedEnvironment ? mockServices : {}),
162+
...services,
163+
}).map((key) => {
164+
try {
165+
return [
166+
key,
167+
isMockedEnvironment && mockServices?.[key]
168+
? mockServices[key]
169+
: services?.[key](),
170+
];
171+
} catch (err) {
172+
if (
173+
err &&
174+
typeof err === 'object' &&
175+
'message' in err &&
176+
typeof err.message === 'string'
177+
)
178+
err.message += ` [locating service '${key}' for '${registryName}']`;
179+
throw err;
180+
}
181+
})
182+
) as Services<S>;
183+
184+
const [{ store, actions }] = useState(
185+
() =>
186+
localAppRegistry.getPlugin(registryName) ??
187+
(() => {
188+
const plugin = config.activate(
189+
props,
190+
{
191+
globalAppRegistry,
192+
localAppRegistry,
193+
...serviceImpls,
194+
},
195+
createActivateHelpers()
196+
);
197+
localAppRegistry.registerPlugin(registryName, plugin);
198+
return plugin;
199+
})()
200+
);
201+
202+
return { store, actions };
203+
}
204+
147205
export type HadronPluginComponent<
148206
T,
149207
S extends Record<string, () => unknown>
150208
> = React.FunctionComponent<T> & {
151209
displayName: string;
210+
211+
/**
212+
* Hook that will activate plugin in the current rendering scope without
213+
* actually rendering it. Useful to set up plugins that are rendered
214+
* conditionally and have to subscribe to event listeners earlier than the
215+
* first render in their lifecycle
216+
*
217+
* @example
218+
* const Plugin = registerHadronPlugin(...);
219+
*
220+
* function Component() {
221+
* Plugin.useActivate();
222+
* const [pluginVisible] = useState(false);
223+
*
224+
* // This Plugin component will already have its store set up and listening
225+
* // to the events even before rendering
226+
* return (pluginVisible && <Plugin />)
227+
* }
228+
*/
229+
useActivate(props: T): void;
230+
152231
/**
153232
* Convenience method for testing: allows to override services and app
154233
* registries available in the plugin context
@@ -228,15 +307,10 @@ export function registerHadronPlugin<
228307
S extends Record<string, () => unknown>
229308
>(config: HadronPluginConfig<T, S>, services?: S): HadronPluginComponent<T, S> {
230309
const Component = config.component;
231-
const registryName = `${config.name}.Plugin`;
232310
const Plugin = (props: React.PropsWithChildren<T>) => {
233311
const isMockedEnvironment = useMockOption('mockedEnvironment');
234312
const mockedPluginName = useMockOption('pluginName');
235313
const disableRendering = useMockOption('disableChildPluginRendering');
236-
const mockServices = useMockOption('mockServices');
237-
238-
const globalAppRegistry = useGlobalAppRegistry();
239-
const localAppRegistry = useLocalAppRegistry();
240314

241315
// This can only be true in test environment when parent plugin is setup
242316
// with mock services. We allow parent to render, but any other plugin
@@ -250,52 +324,11 @@ export function registerHadronPlugin<
250324
return null;
251325
}
252326

253-
const serviceImpls = Object.fromEntries(
254-
Object.keys({
255-
...(isMockedEnvironment ? mockServices : {}),
256-
...services,
257-
}).map((key) => {
258-
try {
259-
return [
260-
key,
261-
isMockedEnvironment && mockServices?.[key]
262-
? mockServices[key]
263-
: services?.[key](),
264-
];
265-
} catch (err) {
266-
if (
267-
err &&
268-
typeof err === 'object' &&
269-
'message' in err &&
270-
typeof err.message === 'string'
271-
)
272-
err.message += ` [locating service '${key}' for '${registryName}']`;
273-
throw err;
274-
}
275-
})
276-
) as Services<S>;
277-
278327
// We don't actually use this hook conditionally, even though eslint rule
279328
// thinks so: values returned by `useMock*` hooks are constant in React
280329
// runtime
281330
// eslint-disable-next-line react-hooks/rules-of-hooks
282-
const [{ store, actions }] = useState(
283-
() =>
284-
localAppRegistry.getPlugin(registryName) ??
285-
(() => {
286-
const plugin = config.activate(
287-
props,
288-
{
289-
globalAppRegistry,
290-
localAppRegistry,
291-
...serviceImpls,
292-
},
293-
createActivateHelpers()
294-
);
295-
localAppRegistry.registerPlugin(registryName, plugin);
296-
return plugin;
297-
})()
298-
);
331+
const { store, actions } = useHadronPluginActivate(config, services, props);
299332

300333
if (isReduxStore(store)) {
301334
return (
@@ -313,6 +346,9 @@ export function registerHadronPlugin<
313346
};
314347
return Object.assign(Plugin, {
315348
displayName: config.name,
349+
useActivate: (props: T) => {
350+
useHadronPluginActivate(config, services, props);
351+
},
316352
withMockServices(
317353
mocks: Partial<Registries & Services<S>>,
318354
options?: Partial<Pick<MockOptions, 'disableChildPluginRendering'>>

0 commit comments

Comments
 (0)