defineSharedStore is the host's half of the contract. One call produces the context plugins bind
to, the per-remote namespacing, the version the manifest is gated against, and the Provider that
PluginRouteLoader mounts for you.
import { defineSharedStore } from "@z0devs/mf-shell-kit";
import { NavRegistry, SettingsRegistry, ComponentRegistry } from "@z0devs/mf-registry";
import { createEventsEmitter } from "@z0devs/mf-context";
import { useAuth, useGlobalStore, contextualApiFetch } from "../app";
const events = createEventsEmitter<{
onAuthChange: { user: User | null };
onOrgChange: { orgId: string };
}>();
export const sharedStore = defineSharedStore({
values: { useAuth, useGlobalStore, contextualApiFetch },
registries: {
nav: NavRegistry.api,
settings: SettingsRegistry.api,
components: ComponentRegistry.api,
},
features: { router: true, onboarding: true },
events,
});
export default sharedStore.useShellSharedStore; // ← the federated expose
export { sharedStore }; // ← host-side accessSchema#
| Field | Required | Type | Notes |
|---|---|---|---|
values | ✅ | Record<string, any> | What plugins may read. Keys are yours to name. Any key matching ^use[A-Z] is treated as a React hook by useSharedValue; everything else is handed back by reference. |
registries | ✅ | Record<string, RegistryApi<any>> | Pass the built-in's .api, not the module. Auto-namespaced per remote in useInitProps. |
features | — | { router?: boolean; onboarding?: boolean } | Library-owned registries. See below. |
events | — | EventsEmitter<TEvents> | From createEventsEmitter<T>(). The library never emits — it is a typed bus for your own lifecycle events. |
apiVersion | — | string | Defaults to SHELL_API_VERSION from @z0devs/mf-core. |
Leave `apiVersion` alone unless you're pinning
The default is what the plugin scaffold targets. Hard-coding a different value blocks every plugin built from the template — see Versioning.
features — registries the library wires for you#
Both are off by default. Enabling one injects its registry into registries, typed, without you
importing its internals.
| Flag | Injects | Also does |
|---|---|---|
router: true | routes: RegistryApi<RouteItem> | Nothing on its own — mount <RouterContextProvider> + <Router> from @z0devs/mf-router to render them. They read the same registry. |
onboarding: true | onboardingSteps: RegistryApi<OnboardingStep> | PluginRouteLoader detects the registry and mounts <OnboardingHost> above its <Outlet/>. |
Do not import routeRegistryApi or onboardingStepsApi yourself — you would get a second
registry the loader doesn't know about.
The returned store#
| Member | Audience | Type / purpose |
|---|---|---|
values | Host | Exactly what you passed in. |
registries | Host | Your registries plus any feature-injected ones. Full RegistryApi — includes host-only emit, setActiveFlags, clearRemote. |
apiVersion | Both | Resolved value: yours, or SHELL_API_VERSION. |
events | Both | The emitter you passed, or undefined. |
Provider | Host | Mount once at the top of the tree. PluginRouteLoader does this for you. |
useShellSharedStore() | Plugins | { values, registries } with registries narrowed to PluginRegistryApi. This is the default export your shell/sharedStore expose must have — the bridge calls it as a hook. |
useInitProps(remoteName) | Host | Same shape, plus every registry wrapped in withRemoteNamespace(remoteName). Fed to <InitPropsProvider>; PluginRouteLoader calls it for you. |
useSharedValue(key) | Both | Auto-invokes use* keys, returns others by reference. |
RegistryApi vs PluginRegistryApi#
The narrowing is the security boundary, not a convenience.
| Capability | Host (RegistryApi) | Plugin (PluginRegistryApi) |
|---|---|---|
getItem / getItems / getActiveFlags | ✅ | ✅ |
addItem / removeItem | ✅ | ✅ — ids auto-prefixed with the remote name |
subscribe | ✅ | ✅ — see below |
setActiveFlags | ✅ | ❌ |
emit / addListener / removeListener | ✅ | ❌ |
clearRemote | ✅ | ❌ |
A plugin therefore cannot flip another plugin's feature flags or wipe its contributions, even by accident.
subscribe is on the plugin side while the emitter is not, and the split is the point: being told the
listed set changed is a read capability. What a remote must not have is emit (a broadcast — it could
fake another plugin's events) or addListener (it could watch another remote's typed traffic). Use it
through the hooks rather than by hand:
import { useRegistryItem, useRegistryItems } from "@z0devs/mf-plugin-kit";
const widget = useRegistryItem(registries.components, "billing_mfe:InvoiceWidget");
const navItems = useRegistryItems(registries.nav);`useSharedValue` and the rules of hooks
Call it with the same key on every render of a component. useSharedValue("useAuth") invokes a
hook; useSharedValue("contextualApiFetch") returns a function. Switching keys between renders
changes the call shape and breaks the hooks invariant. For dynamic access read
useShellSharedStore().values[key] and invoke it yourself.
Exported types#
| Type | What it describes |
|---|---|
SharedStoreSchema<TValues, TRegistries, TEvents> | The argument object. |
DefinedSharedStore<TValues, TRegistries, TEvents> | The return value. Plugins import typeof sharedStore to type their bridge. |
SharedInitProps<TValues, TRegistries> | { values, registries } with full registries — host side. |
PluginInitProps<TValues, TRegistries> | Same, registries narrowed to PluginRegistryApi — what a remote's init receives. |
StoreFeatures | The features flags. |
WithFeatureRegistries<TRegistries, TFeatures> | Your registries after feature injection. |
AnyRegistry | RegistryApi<any> — the constraint registries values must satisfy. |
These live in @z0devs/mf-shell-kit, a host package. Plugins don't install it — they declare a
structural InitProps instead, for the reasons in
Build a plugin.