intentx-solid enforces a strict architectural boundary between deterministic business logic and reactive UI in Solid.
It enforces a strict separation between:
- Business Logic (deterministic runtime)
- UI Rendering (fine-grained reactivity)
It is a bridge between deterministic logic and Solidβs reactive UI.
Use it when your UI starts to feel like business logic.
β
Complex async workflows
β
Intent-based architecture
β
Microfrontend communication
β
Testable business logic
β
Cross-framework runtime reuse
Avoid it when:
β This library introduces an architectural boundary.
β If you donβt need architectural boundaries, donβt use it.
UI / HTTP / Queue / Cron
β
intentx-runtime
β
Fine-grained reactivity updates UICore principle:
Intent is the only mutation entry point. The runtime owns behavior. UI only triggers intent.
npm install intentx-solidEven though this is the React package, the runtime is fully usable without React.
import { createLogic } from "intentx-solid"
export const counterLogic = createLogic({
name: "counter",
state: {
count: 0
},
computed: {
double: ({ state }) => state.count * 2
},
intents: (bus) => {
bus.on("inc", ({ setState }) => {
setState((s) => {
s.count++;
});
});
},
actions: {
inc({ emit }) {
return () => emit("inc");
},
},
})import { useLogic } from "intentx-solid"
import { counterLogic } from "./counter.logic"
export default function Counter() {
const counter = useLogic(counterLogic)
// const { actions, emit, store } = useLogic(counterLogic)
return (
<>
<button onClick={counter.actions.inc}>
{counter.state.count}
</button>
<button onClick={() => emit("inc")}>
{counter.state.count}
</button>
<p>Double: {counter.store.double}</p>
</>
)
}No wrapper components. No providers required (unless you want shared context).
const counter = useLogic(counterLogic) {
store, //
state, // alias state
actions, // actions -> emit
emit // directly
}π₯ Important
-
store is readonly
-
Mutations must go through actions
-
Solid reactivity remains fine-grained
Each logic instance is isolated by default.
To enable communication between runtimes, you can share an Intent Bus.
import { useLogic } from "intentx-solid"
// β
Same scope β shared bus
useLogic(logic, {
scope: "dashboard",
sharedBus: true
})
// β Different scope β different bus
useLogic(logic, {
scope: "settings",
sharedBus: true
})How it works
When sharedBus: true is enabled:
-
A singleton bus is created per scope
-
Same scope β same bus instance
-
Different scope β different bus
-
No global leakage
If no scope is provided:
useLogic(logic, {
sharedBus: true
})β uses a default global scope bus.
import { createIntentBus } from "intentx-react"
const bus = createIntentBus()
useLogic(logicA, { bus })
useLogic(logicB, { bus })Behavior
- Full cross-scope communication
- Manual orchestration control
- Suitable for:
- Microfrontend
- App-wide coordination
- Complex workflow systems
| Mode | Isolation | Scope-aware | Cross-scope | Recommended |
|---|---|---|---|---|
| Default (no options) | β Full | β | β | Small/local logic |
sharedBus: true |
β Per scope | β | β | Modular apps |
Custom bus |
β Manual | β | β | Advanced architecture |
β
Use sharedBus for modular communication.
β
Use custom bus for orchestration layer.
π« Avoid global single bus without scope in large apps.
Provide logic via Solid context:
import { setLogicContext, useLogicContext } from "intentx-solid"
import { counterLogic } from "./counter.logic"Provider:
const { Provider } = setLogicContext(
"counter",
counterLogic
)
export default function App() {
return (
<Provider>
<Child />
</Provider>
)
}Consume:
const counter = useLogicContext("counter")- Runtime created during SSR
- Deterministic snapshot
- Hydration-safe
- No client-only hacks
- Server snapshot serializable
- Client rehydrates from deterministic state
Works with:
- SolidStart
- Node SSR
- Edge runtimes
Without rendering anything:
const runtime = counterLogic.create()
runtime.actions.inc()
expect(runtime.state.count).toBe(1)That is the real split.
| Criteria | Solid primitives | intentx-solid |
|---|---|---|
| Local UI | Excellent | Overkill |
| Async orchestration | Manual | Structured |
| Cross-environment reuse | No | Yes |
| Deterministic runtime | No | Yes |
- UI consumes state.
- Logic lives outside components.
It separates business logic from UI completely.
Without this split:
- Components start holding async workflows
- Event chains become implicit
- State transitions become coupled to rendering
- Testing requires rendering components
With intentx-solid:
- Logic lives outside the component tree
- UI becomes a pure consumer
- Async workflows are deterministic
- Runtime can be reused in Node, SSR, microfrontends
Solid owns:
- Signals
- Reactivity
- DOM updates
- Rendering lifecycle
intentx owns:
- State machine
- Intent routing
- Async orchestration
- Computed graph
- Cross-runtime communication
This is not about replacing createSignal.
It is about preventing this:
// β Business logic leaking into UI
createEffect(async () => {
if (userId()) {
const data = await fetchUser(userId())
setUser(data)
}
})And moving it here:
// β
Logic outside UI
intents: (bus) => {
bus.on("loadUser", async ({ setState }) => {
const data = await fetchUser(id)
setState(d => {
d.user = data
})
});
}Now UI only emits intent.
Rendering is reactive.
Business logic should be deterministic.
intentx-solid ensures they never mix.
- Not a signal replacement
- Not a store wrapper
- Not a UI state helper
- Not a reducer abstraction
MIT