Portability
Rimitive is designed so the same code can run in different contexts without modification.
Depend on Contracts, Not Frameworks
Section titled “Depend on Contracts, Not Frameworks”A portable component depends on a service contract—a set of dependencies it needs—not a specific framework:
// This component depends on { el, signal, computed }// It doesn't know or care where those come fromconst Counter = (svc: { el: ElFactory; signal: SignalFactory; computed: ComputedFactory }) => () => { const { el, signal, computed } = svc; const count = signal(0);
return el('div')( el('span')(computed(() => `Count: ${count()}`)), el('button').props({ onclick: () => count(count() + 1) })('Increment') );};This component works with any service that provides el, signal, and computed—whether that’s a DOM view service, a test service, or something custom.
Portable Component Pattern
Section titled “Portable Component Pattern”The standard pattern is a function that receives a service and returns a function that takes props:
// (svc) => (props) => RefSpecconst Button = (svc: Service) => (props: { label: string; onClick: () => void }) => { const { el } = svc;
return el('button').props({ onclick: props.onClick })(props.label);};This shape lets you:
- Partially apply the service once
- Call the resulting function multiple times with different props
- Pass the component to
use()for ergonomic instantiation
const App = (svc: Service) => () => { const { el, use } = svc;
return el('div')( use(Button)({ label: 'Save', onClick: handleSave }), use(Button)({ label: 'Cancel', onClick: handleCancel }) );};Running in Different Contexts
Section titled “Running in Different Contexts”Rimitive DOM View
Section titled “Rimitive DOM View”import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule, BatchModule } from '@rimitive/signals/extend';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { MountModule } from '@rimitive/view/deps/mount';
// Create adapter and compose serviceconst adapter = createDOMAdapter();const svc = compose( SignalModule, ComputedModule, EffectModule, BatchModule, createElModule(adapter), MountModule)();
const app = Counter(svc);document.body.appendChild(svc.mount(app).element!);Testing Without DOM
Section titled “Testing Without DOM”For tests, use a mock adapter or compose with test dependencies:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
// Minimal test service - just signals, no view layerconst testSvc = compose(SignalModule, ComputedModule, EffectModule)();
// Test a behavior directlyconst { count, increment } = counter(testSvc)(0);expect(count()).toBe(0);increment();expect(count()).toBe(1);React (via @rimitive/react)
Section titled “React (via @rimitive/react)”Portable behaviors work in React through createHook:
import { SignalProvider, createHook, useSubscribe } from '@rimitive/react';import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
// Define a portable behaviorconst counter = (svc: { signal: SignalFactory }) => (initial = 0) => { const count = svc.signal(initial); return { count, increment: () => count(count() + 1), decrement: () => count(count() - 1), };};
// Turn it into a React hookconst useCounter = createHook(counter);
function ReactCounter() { const { count, increment } = useCounter(0); const value = useSubscribe(count); // Subscribe to trigger re-renders
return <button onClick={increment}>Count: {value}</button>;}
// Wrap your app with SignalProviderconst svc = compose(SignalModule, ComputedModule, EffectModule)();
function App() { return ( <SignalProvider svc={svc}> <ReactCounter /> </SignalProvider> );}Custom Renderer
Section titled “Custom Renderer”The same pattern works with any adapter:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { createElModule } from '@rimitive/view/el';import { myCanvasAdapter } from './my-canvas-adapter';
const svc = compose( SignalModule, ComputedModule, EffectModule, createElModule(myCanvasAdapter))();
const app = Counter(svc);// Renders to canvas instead of DOMAnti-patterns
Section titled “Anti-patterns”Don’t Import Framework-Specific Code Directly
Section titled “Don’t Import Framework-Specific Code Directly”Importing directly couples your code to a specific setup:
// ❌ WRONG - hardcoded dependency, not portableimport { signal } from '@rimitive/signals';
const MyComponent = () => { const count = signal(0); // Can't swap this out for testing};// ✅ CORRECT - depend on the service contractconst MyComponent = (svc: Service) => () => { const { signal } = svc; const count = signal(0); // Works with any compatible service};Don’t Hardcode External Dependencies
Section titled “Don’t Hardcode External Dependencies”Hardcoding URLs, API calls, or other external dependencies makes code untestable and inflexible:
// ❌ WRONG - hardcoded URL, can't test without networkconst DataList = (svc: Service) => () => { const items = svc.resource(() => fetch('/api/items'));};// ✅ CORRECT - accept dependencies via service or propsconst DataList = (svc: Service & { fetchItems: () => Promise<Item[]> }) => () => { const items = svc.resource(svc.fetchItems);};
// Or via propsconst DataList = (svc: Service) => (props: { fetchItems: () => Promise<Item[]> }) => { const items = svc.resource(props.fetchItems);};This lets you inject a mock fetcher in tests:
const mockSvc = { ...testSvc, fetchItems: async () => [{ id: 1, name: 'Test' }],};Portable Behaviors vs Portable Components
Section titled “Portable Behaviors vs Portable Components”Portable behaviors return reactive state and logic—no UI:
const counter = (svc: { signal: SignalFactory }) => (initial = 0) => { const count = svc.signal(initial); return { count, increment: () => count(count() + 1), decrement: () => count(count() - 1), };};Portable components return UI specs:
const Counter = (svc: Service) => () => { const { el, signal, computed } = svc; const count = signal(0);
return el('div')( el('span')(computed(() => count())), el('button').props({ onclick: () => count(count() + 1) })('+') );};Behaviors are more portable than components because they have no UI coupling. A behavior can be used in Rimitive views, React, Vue, or anywhere else that can consume signals.
Type Safety
Section titled “Type Safety”Define your service contracts with TypeScript:
import type { SignalFactory, ComputedFactory } from '@rimitive/signals';import type { ElFactory } from '@rimitive/view/el';import type { DOMAdapterConfig } from '@rimitive/view/adapters/dom';
// Minimal contract for a componenttype MinimalSvc = { signal: SignalFactory; computed: ComputedFactory; el: ElFactory<DOMAdapterConfig>;};
// Component declares exactly what it needsconst MyComponent = (svc: MinimalSvc) => { /* ... */ };This makes dependencies explicit and enables better tree-shaking—if a component only needs signal and el, it doesn’t pull in effect, resource, etc.
Testing Portable Code
Section titled “Testing Portable Code”Portable code is easy to test because you control the service:
import { describe, it, expect } from 'vitest';import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
describe('counter behavior', () => { it('increments count', () => { const svc = compose(SignalModule, ComputedModule, EffectModule)(); const { count, increment } = counter(svc)(0);
expect(count()).toBe(0); increment(); expect(count()).toBe(1); });});No DOM mocking, no framework test utilities—just functions and assertions.