Skip to content

Portability

Rimitive is designed so the same code can run in different contexts without modification.


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 from
const 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.


The standard pattern is a function that receives a service and returns a function that takes props:

// (svc) => (props) => RefSpec
const 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 })
);
};

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 service
const adapter = createDOMAdapter();
const svc = compose(
SignalModule, ComputedModule, EffectModule, BatchModule,
createElModule(adapter), MountModule
)();
const app = Counter(svc);
document.body.appendChild(svc.mount(app).element!);

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 layer
const testSvc = compose(SignalModule, ComputedModule, EffectModule)();
// Test a behavior directly
const { count, increment } = counter(testSvc)(0);
expect(count()).toBe(0);
increment();
expect(count()).toBe(1);

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 behavior
const 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 hook
const 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 SignalProvider
const svc = compose(SignalModule, ComputedModule, EffectModule)();
function App() {
return (
<SignalProvider svc={svc}>
<ReactCounter />
</SignalProvider>
);
}

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 DOM

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 portable
import { signal } from '@rimitive/signals';
const MyComponent = () => {
const count = signal(0); // Can't swap this out for testing
};
// ✅ CORRECT - depend on the service contract
const MyComponent = (svc: Service) => () => {
const { signal } = svc;
const count = signal(0); // Works with any compatible service
};

Hardcoding URLs, API calls, or other external dependencies makes code untestable and inflexible:

// ❌ WRONG - hardcoded URL, can't test without network
const DataList = (svc: Service) => () => {
const items = svc.resource(() => fetch('/api/items'));
};
// ✅ CORRECT - accept dependencies via service or props
const DataList = (svc: Service & { fetchItems: () => Promise<Item[]> }) => () => {
const items = svc.resource(svc.fetchItems);
};
// Or via props
const 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 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.


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 component
type MinimalSvc = {
signal: SignalFactory;
computed: ComputedFactory;
el: ElFactory<DOMAdapterConfig>;
};
// Component declares exactly what it needs
const 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.


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.