Composability Map
A default-path guide to composing @logixjs/core + @logixjs/react (Module / Logic / Runtime / React).
This page is not “yet another way to do things”. It’s a map of existing Logix composition points, answering the questions you’ll hit in real apps:
- Should this be a singleton or multi-instance?
- If a module composes child modules, how does the UI get the child within the parent instance scope?
- Should cross-module collaboration live inside a module (
$.use) or outside modules (Process.link / Link.make)? - When do you need subtree overrides like
RuntimeProvider.layer?
One-sentence rules (prefer the default path)
- Make singleton vs multi-instance explicit:
useModule(ModuleDef/ModuleTag)is a Provider-scoped singleton;useModule(Impl, { key })is multi-instance. - For parent-child composition, prefer imports (strict scope): children follow the parent instance; Logic uses
$.use(Child), UI useshost.imports.get(Child.tag). - For “runtime-owned” glue logic, prefer processes: when collaboration belongs to the runtime (not a single module), attach it via
processes. - UI should bind to a Host by default: don’t “climb the imports tree” everywhere; resolve once at the boundary and pass
ModuleRefdown.
Decision tree (Mermaid)
Composition cheat sheet (what should I use?)
| What you’re solving | Recommended entry | Scope intuition | Where it lives | Read more |
|---|---|---|---|---|
Local UI state (replace useState/useReducer) | useLocalModule | component lifetime | React component | React integration |
| Page/session multi-instance (tabs/sessions) | useModule(Impl, { key }) | one instance per (Impl, key) | React component | React integration |
| Parent-child composition (children follow parent) | imports + $.use(Child) | strict imports scope | ModuleImpl / Logic | Cross-module communication |
| UI reads child within parent instance scope | host.imports.get(Child.tag) / useImportedModule | parent instance scope | React component | useImportedModule, Route-scope modals |
| Long-running cross-module collaboration (runtime glue) | Process.link / Link.make | runtime processes | Root processes | Cross-module communication, Runtime |
| Fixed root singleton read | Logix.Root.resolve(Tag) | fixed root provider | Logic | Cross-module communication |
| Subtree env override (light config) | RuntimeProvider layer={...} | subtree env override | React tree | Runtime |
| Caller DX (author-side) | handle extensions | extra handle fields | module author code | Module handle extensions |
| Reuse “logic snippets / flow templates” | Pattern functions | (config) => Effect or ($, config) => ... | guide/patterns / helpers | Pattern example |
Common footguns (avoid these)
- Treating an imports child as a global singleton: writing
useModule(Child)/useModule(Child.tag)when you actually needhost.imports.get(Child.tag). - Using Link/Process to “pick an instance”: processes are for singleton sets; for multi-instance collaboration, let the owner module drive it within its instance scope via
$.use(...). - Climbing the imports tree everywhere:
host.imports.get(A).imports.get(B)...scatters dependencies; resolve once at the boundary and passModuleRefdown. - Expecting local overrides to affect root singletons:
Root.resolvereads from the fixed root provider; if you need override semantics, use imports orRuntimeProvider.layer.
Next
- To understand the semantics of
imports / $.use / Link / Root.resolve: read Cross-module communication. - To apply composition in React code: read React integration and Route-scope modals keepalive.
- To package high-frequency operations into ergonomic call sites: read Module handle extensions.