Skip to content

Custom Modules

Package on GitHub

You can define your own modules with defineModule and compose them alongside signals, view, or anything else.


A module needs a name and a create function:

import { defineModule, compose } from '@rimitive/core';
const LoggerModule = defineModule({
name: 'logger',
create: () => ({
log: (msg: string) => console.log(`[LOG] ${msg}`),
error: (msg: string) => console.error(`[ERROR] ${msg}`),
}),
});
const svc = compose(LoggerModule);
svc.logger.log('hello');

Modules can depend on other modules. List them in dependencies, and they’ll be available in create:

import { defineModule, compose } from '@rimitive/core';
import { SignalModule, ComputedModule } from '@rimitive/signals/extend';
const CounterModule = defineModule({
name: 'counter',
dependencies: [SignalModule, ComputedModule],
create: ({ signal, computed }) => (initial = 0) => {
const count = signal(initial);
const doubled = computed(() => count() * 2);
return {
count,
doubled,
increment: () => count(count() + 1),
decrement: () => count(count() - 1),
};
},
});
const svc = compose(CounterModule);
const myCounter = svc.counter(10);
myCounter.count(); // 10
myCounter.increment();
myCounter.count(); // 11
myCounter.doubled(); // 22

Dependencies are resolved automatically. You only need to pass the modules you want—compose() includes transitive dependencies.


Modules can hook into lifecycle events:

const ConnectionModule = defineModule({
name: 'connection',
create: () => {
const ws = new WebSocket('wss://example.com');
return {
send: (msg: string) => ws.send(msg),
socket: ws,
};
},
init: (ctx) => {
// Called before create, useful for setup
},
destroy: (ctx) => {
// Called when svc.dispose() is invoked
},
});
const svc = compose(ConnectionModule);
// ... use the connection
svc.dispose(); // triggers destroy hooks

Here’s the thing: @rimitive/core has no concept of reactivity. It’s just a composition mechanism. You can use it for anything:

import { defineModule, compose } from '@rimitive/core';
const HttpModule = defineModule({
name: 'http',
create: () => ({
get: (url: string) => fetch(url).then(r => r.json()),
post: (url: string, data: unknown) =>
fetch(url, { method: 'POST', body: JSON.stringify(data) }).then(r => r.json()),
}),
});
const CacheModule = defineModule({
name: 'cache',
create: () => {
const store = new Map<string, unknown>();
return {
get: <T>(key: string): T | undefined => store.get(key) as T,
set: <T>(key: string, value: T): void => { store.set(key, value); },
clear: () => store.clear(),
};
},
});
const ApiModule = defineModule({
name: 'api',
dependencies: [HttpModule, CacheModule],
create: ({ http, cache }) => ({
async getUser(id: string) {
const cached = cache.get<User>(`user:${id}`);
if (cached) return cached;
const user = await http.get(`/api/users/${id}`);
cache.set(`user:${id}`, user);
return user;
},
}),
});
const svc = compose(ApiModule);
await svc.api.getUser('123');

This makes compose() useful for any library or application architecture—not just UI frameworks. You control what gets composed.


Only the modules you compose are included in your bundle.


Create a module when you want to:

  • Add new modules to a composed service
  • Share infrastructure (logging, http, storage) across behaviors
  • Encapsulate setup/teardown with lifecycle hooks
  • Build your own libraries on top of composition

For reusable reactive logic, prefer behaviors. Behaviors are simpler and don’t require defineModule. Use modules when you need something that lives at the composition level.