ArtisanPack UI
DocsHooks - JSModule Federation

Module Federation

Hooks - JS v1.0.0

Hooks are only useful when host and remote agree on a single registry. Two bundled copies of this package can share a registry via — but the safest and...

Hooks are only useful when host and remote agree on a single registry. Two bundled copies of this package can share a registry via globalThis — but the safest and most explicit path is to declare the package as a singleton in your bundler.

See also: Getting Started and React.

The problem

Every runtime-loaded remote in a Module-Federation-style setup normally ships its own copy of every dependency. If both the host and a remote bundle @artisanpack-ui/hooks-js, and their bundlers do nothing special, each copy would ordinarily have its own module-scope map and see none of the other copy's callbacks.

@artisanpack-ui/hooks-js sidesteps this by installing its registry on a Symbol.for('@artisanpack-ui/hooks-js/singleton') slot on globalThis, so duplicate copies do share state. But it is still strongly preferred to configure your bundler to load a single copy — you get proper tree-shaking, one set of types, and one canonical function reference for removeAction/removeFilter.

Bundler configuration

@originjs/vite-plugin-federation

federation({
  name: 'host',
  shared: {
    '@artisanpack-ui/hooks-js':       { singleton: true, strictVersion: false },
    '@artisanpack-ui/hooks-js/react': { singleton: true, strictVersion: false },
  },
});

webpack ModuleFederationPlugin

new ModuleFederationPlugin({
  name: 'host',
  shared: {
    '@artisanpack-ui/hooks-js':       { singleton: true, requiredVersion: false },
    '@artisanpack-ui/hooks-js/react': { singleton: true, requiredVersion: false },
  },
});

Rspack ModuleFederationPlugin

new rspack.container.ModuleFederationPlugin({
  name: 'host',
  shared: {
    '@artisanpack-ui/hooks-js':       { singleton: true, requiredVersion: false },
    '@artisanpack-ui/hooks-js/react': { singleton: true, requiredVersion: false },
  },
});

strictVersion: false / requiredVersion: false avoids a load error when host and remote ship different minor or patch versions. The registries are compatible across minor versions by design; only the public function signatures matter, and those are stable within a major.

globalThis.ApHooks escape hatch

If a remote is loaded outside your bundler graph (a browser extension, a runtime-injected <script>, a plugin whose author cannot patch their federation config), it still needs a way to reach the host's registry. The package publishes its public API on globalThis.ApHooks on first import:

// Anywhere on the page, no import required:
window.ApHooks.addAction('order.placed', (order) => sendReceipt(order));
window.ApHooks.doAction('order.placed', order);

window.ApHooks and import { addAction } share the same underlying registry — via the Symbol.for('@artisanpack-ui/hooks-js/singleton') slot on globalThis — so a callback added through one route fires when dispatched through the other, even across duplicate copies of the package.

The full API surface is available on window.ApHooks: addAction, doAction, removeAction, removeAllActions, hasAction, addFilter, applyFilters, removeFilter, removeAllFilters, hasFilter, hasHook, deprecateHook, hasAliases, aliasesFor, resetDeprecationLogState, and version.

Prefer the shared: singleton config; ApHooks is the fallback.

Plugin bootModule recipe

Runtime-loaded plugins should register their hooks before the first page mounts, so callbacks are in place the moment the host emits its first action or applies its first filter. A bootModule — a dedicated entry that only wires up hooks — makes that ordering explicit:

// plugin/src/boot.ts
import { addAction, addFilter } from '@artisanpack-ui/hooks-js';

export function bootModule(): void {
  addAction('app.ready', () => {
    // side effects that need to run once the host is up
  });

  addFilter('nav.items', (items) => [...items, { label: 'My plugin', href: '/plugin' }]);
}
// host/src/bootstrap.ts
import { bootModule } from 'plugin/boot';

async function bootstrap(): Promise<void> {
  bootModule();               // register hooks first
  await import('./app');      // now mount the app — its actions/filters see the plugin
}

bootstrap();

Plugins that cannot ship a bundler-integrated bootModule can achieve the same ordering by reading window.ApHooks and registering as soon as their <script> loads, provided that script is included ahead of the app bundle.

Version introspection

Every copy of the package exports a VERSION string and exposes the same value at window.ApHooks.version. When the escape hatch is installed by an older copy first, a newer copy's installGlobal backfills any missing methods without overwriting existing keys — this preserves whichever function reference won the race while ensuring every documented method is callable.


Continue to DevTools Debug