Rimitive
Rimitive
Section titled “Rimitive”DISCLAIMER: This is alpha software—it’s heavily tested and benchmarked, but the usual disclaimers apply.
The Core Idea
Section titled “The Core Idea”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 primitivessvc.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.
What Does That Unlock?
Section titled “What Does That Unlock?”Rimitive provides pre-built modules for reactivity and UI:
| Package | Modules | What they provide |
|---|---|---|
@rimitive/signals | SignalModule, ComputedModule, EffectModule | Reactive state & effects |
@rimitive/view | createElModule, createMapModule, createMatchModule | UI 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)
Using the Primitives
Section titled “Using the Primitives”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.
Going Deeper: Custom Modules
Section titled “Going Deeper: Custom Modules”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 adaptersconst 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.
Patterns: Behaviors
Section titled “Patterns: Behaviors”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:
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:
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!
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 behaviorsconst 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.
Specs and Mounting
Section titled “Specs and Mounting”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 elementconst Button = (label: string) => el('button')( // provide the children as arguments label );
// Specs can be stored and reusedconst save = Button('Save');const cancel = Button('Cancel');
// mount() turns specs into real DOM elementsdocument.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.
Extensibility
Section titled “Extensibility”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
Adapterinterface 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.
Inspirations
Section titled “Inspirations”Rimitive draws from libraries that shaped how I think about reactivity and composition:
- alien-signals and Reactively — push-pull reactivity, graph coloring
- downshift — headless, portable UI behavior
- jotai — atoms as configs, not values
- ProseMirror — extensibility and determinism
Status
Section titled “Status”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!