Skip to content

Rimitive

Portable reactive specs, composable services.

DISCLAIMER: This is alpha software—it’s heavily tested and benchmarked, but the usual disclaimers apply.

Rimitive is built on two core concepts: modules and composition.

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule } from '@rimitive/signals/extend';
const svc = compose(SignalModule, ComputedModule);
svc.signal(0); // access the primitives
svc.computed(() => …); // through the composed service
  • A module defines a primitive and its dependencies
  • Composition resolves the dependency graph and creates a service

At its core, that’s it. compose() is the backbone of Rimitive: a simple, type-safe way to wire modules together. Dependencies are resolved automatically—you pass what you need, and Rimitive figures out the rest.


Rimitive provides pre-built modules for reactivity and UI:

PackageModulesWhat they provide
@rimitive/signalsSignalModule, ComputedModule, EffectModuleReactive state & effects
@rimitive/viewcreateElModule, createMapModule, createMatchModuleUI specs

These primitives produce different outputs:

  • signal(0) → reactive state (live, tracks dependencies)
  • computed(() => ...) → derived reactive value (live, lazy)
  • effect(() => ...) → side effect (runs synchronously when dependencies change)
  • el('div')(...) → spec (inert blueprint, needs mounting)
  • map(items, ...) → fragment spec (inert, needs mounting)

Compose the modules you need, then destructure the primitives:

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
import { createDOMAdapter } from '@rimitive/view/adapters/dom';
import { createElModule } from '@rimitive/view/el';
import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();
const svc = compose(
SignalModule,
ComputedModule,
EffectModule,
createElModule(adapter),
MountModule
);
const { signal, computed, el, mount } = svc;
const App = () => {
const count = signal(0);
return el('div')(
el('p').props({ textContent: computed(() => `Count: ${count()}`) }),
el('button').props({ onclick: () => count(count() + 1) })('Increment')
);
};
document.body.appendChild(mount(App()).element!);

View modules like createElModule take an adapter—that’s how Rimitive stays renderer-agnostic. Swap the DOM adapter for a Canvas adapter, or a test adapter, or your own. You control the composition down to the very base reactive model and renderer itself.


Need to share signals across multiple renderers? Compose them into each service:

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
import { createDOMAdapter } from '@rimitive/view/adapters/dom';
import { createElModule } from '@rimitive/view/el';
// Same signal modules, different adapters
const domAdapter = createDOMAdapter();
const canvasAdapter = createCanvasAdapter();
const domService = compose(
SignalModule, ComputedModule, EffectModule,
createElModule(domAdapter)
);
const canvasService = compose(
SignalModule, ComputedModule, EffectModule,
createElModule(canvasAdapter)
);

That’s how Rimitive handles SSR—swapping the DOM adapter for a server adapter. Or write your own modules with defineModule:

import { defineModule } from '@rimitive/core';
const Logger = defineModule({
name: 'logger',
create: () => ({
log: (msg: string) => console.log(msg),
}),
});
const svc = compose(SignalModule, Logger);
svc.logger.log('hello');

You control the composition. A natural benefit is that everything is fully tree-shakeable. Just need signals without a view layer? Compose only what you need.


Once you have primitives and services, patterns emerge. One such pattern is the behavior—a portable function that receives a service and returns a reactive API:

behaviors/counter.ts
export const counter =
({ signal, computed }: SignalsSvc) =>
(initial = 0) => {
const count = signal(initial);
const doubled = computed(() => count() * 2);
return {
count,
doubled,
increment: () => count(count() + 1),
};
};

The behavior defines what the logic is. The service provides the primitives. And behaviors can compose other behaviors. Consider open/close state—the same logic applies to accordions, dropdowns, modals, tooltips. Capture it once:

behaviors/disclosure.ts
export const disclosure =
({ signal, computed }: SignalsSvc) =>
(initialOpen = false) => {
const isOpen = signal(initialOpen);
return {
isOpen,
open: () => isOpen(true),
close: () => isOpen(false),
toggle: () => isOpen(!isOpen()),
triggerProps: computed(() => ({ 'aria-expanded': isOpen() })),
contentProps: computed(() => ({ hidden: !isOpen() })),
};
};

Now a dropdown behavior can compose this with keyboard handling:

NOTE: The below example names the behavior useDisclosure, but it’s NOT a React hook and the “rules of hooks” do not apply, either in naming or usage. There’s no magic here going on, it’s just returning the api object above. I just think React nailed a naming convention. Name it whatever you want!

behaviors/dropdown.ts
export const dropdown = (svc: SignalsSvc) => {
// Behaviors can compose other behaviors by passing the service through
const useDisclosure = disclosure(svc);
return (options?: { initialOpen?: boolean }) => {
const d = useDisclosure(options?.initialOpen ?? false);
const handlers = {
onKeyDown: (e: KeyboardEvent) => {
if (e.key === 'Escape') d.close();
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
d.toggle();
}
},
};
return { ...d, handlers };
};
};
// Usage: compose() returns a service that can call behaviors
const svc = compose(SignalModule, ComputedModule);
const useDropdown = svc(dropdown);
const dd = useDropdown({ initialOpen: false });

The same disclosure behavior could be composed into an accordion, modal, or tooltip—each adding its own semantics on top.

Because behaviors only depend on the service contract (not a specific framework), they’re portable. The same behavior works in Rimitive views, React (via @rimitive/react), or any other integration that provides the service.


UI primitives like el and map produce specs—inert data structures that describe UI:

NOTE: Again, this is NOT React! Functions shown here are not reactive closures that “re-render” like in other frameworks. There’s no implicit re-execution. Reactivity lives in the primitives (signal, computed), not in component (or behavior) functions.

// This returns a spec, not a DOM element
const Button = (label: string) =>
el('button')(
// provide the children as arguments
label
);
// Specs can be stored and reused
const save = Button('Save');
const cancel = Button('Cancel');
// mount() turns specs into real DOM elements
document.body.appendChild(mount(el('div')(save, cancel)).element!);

Specs don’t become real elements until mounted with an adapter. The same spec can be mounted with different adapters (DOM, SSR, test, etc) or composed into larger specs before mounting. A happy side-effect of this design is that it makes SSR straightforward.


You own the composition layer. Want to:

  • Create custom modules? Use defineModule() with the same patterns Rimitive uses internally
  • Swap out the reactive system? Replace the dependency modules with your own (or someone else’s)
  • Build a custom adapter/renderer? Implement the Adapter interface for Canvas, WebGL, or anything tree-based
  • Add instrumentation? Compose with createInstrumentation() for debugging; instrumentation is first-class in Rimitive

Rimitive provides modules for reactivity and UI out of the box, but they’re not special—they’re built with the same tools you have access to. In fact, Rimitive at its core is a simple, type-safe composition pattern, so it can be used for creating lots of tools, not just reactive frameworks.


Rimitive draws from libraries that shaped how I think about reactivity and composition:


Alpha. Heavily tested and benchmarked, but not battle-tested in production. If you’re interested in composable reactivity and portable patterns, take a look around and hit me up!