Skip to content

Shared State

In Rimitive, the service is the context. Components receive a service object containing the modules they need. You can extend that service with additional state at any point in the tree.

// Define your app-level shared state
type AppService = Service & {
theme: Readable<'light' | 'dark'>;
user: Readable<User | null>;
};
const App = (svc: Service) => () => {
const { el, signal } = svc;
// Create shared state
const theme = signal<'light' | 'dark'>('light');
const user = signal<User | null>(null);
// Extend the service
const appSvc: AppService = { ...svc, theme, user };
return el('div')(
Header(appSvc)(),
Main(appSvc)(),
Footer(appSvc)()
);
};

Child components receive the extended service and can access the shared state directly:

const Header = (svc: AppService) => () => {
const { el, computed, theme, user } = svc;
return el('header').props({
className: computed(() => theme() === 'dark' ? 'header-dark' : 'header-light')
})(
el('span')(computed(() => user()?.name ?? 'Guest'))
);
};

You can see exactly what’s being passed where.


Need to override a value for a subtree? Extend the service again:

const DarkSection = (svc: AppService) => () => {
const { el, signal } = svc;
// Override theme for this subtree
const darkSvc: AppService = { ...svc, theme: signal('dark') };
return el('section')(
// Everything in here sees theme = 'dark'
ThemedCard(darkSvc)(),
ThemedButton(darkSvc)()
);
};

The portable component pattern ((svc) => (props) => spec) works naturally with this:

// Portable component that expects theme in service
const ThemedButton = ({ el, computed, theme }: AppService) => (props: { label: string }) => {
return el('button').props({
className: computed(() => `btn btn-${theme()}`)
})(props.label);
};
// Usage with `use`
const App = ({ el, use }: AppService) => () => {
return el('div')(
use(ThemedButton)({ label: 'Click me' })
);
};

When you call use(ThemedButton), it passes the current service (including any extensions) to the component.


Define your extended service types explicitly:

import type { Service } from './service';
import type { Readable } from '@rimitive/signals/types';
// Base service with your extensions
export type AppService = Service & {
theme: Readable<'light' | 'dark'>;
user: Readable<User | null>;
// add more as needed
};
// For components that only need a subset
export type ThemedService = Service & {
theme: Readable<'light' | 'dark'>;
};

Components declare what they need:

// This component works with any service that has `theme`
const ThemedCard = (svc: ThemedService) => () => { ... };
// This one needs the full app service
const UserProfile = (svc: AppService) => () => { ... };

Don’t Use Module-Level Signals for Shared State

Section titled “Don’t Use Module-Level Signals for Shared State”

Creating signals at module scope seems convenient, but it causes state to leak across your entire application and makes testing nearly impossible:

// ❌ WRONG - module-level signal is global singleton
const theme = signal<'light' | 'dark'>('light');
const user = signal<User | null>(null);
const Header = (svc: Service) => () => {
// Reads global state - can't test in isolation
return svc.el('header')(theme());
};
// ✅ CORRECT - state lives in the service, threaded explicitly
const App = (svc: Service) => () => {
const theme = svc.signal<'light' | 'dark'>('light');
const user = svc.signal<User | null>(null);
const appSvc = { ...svc, theme, user };
return svc.el('div')(Header(appSvc)());
};

The service threading pattern keeps state local to component trees and enables testing with isolated services.

Mutating the service directly instead of creating a new one causes subtle bugs—all components sharing that service see the mutation:

// ❌ WRONG - mutates shared service object
const DarkSection = (svc: AppService) => () => {
svc.theme = svc.signal('dark'); // Mutates the original!
return svc.el('section')(/* ... */);
};
// ✅ CORRECT - create a new extended service
const DarkSection = (svc: AppService) => () => {
const darkSvc = { ...svc, theme: svc.signal('dark') };
return svc.el('section')(Child(darkSvc)());
};

Don’t Skip Type Annotations for Extended Services

Section titled “Don’t Skip Type Annotations for Extended Services”

Without proper types, you lose the main benefit of explicit service threading—knowing exactly what’s available:

// ❌ WRONG - untyped extension loses type safety
const App = (svc: Service) => () => {
const theme = svc.signal('light');
const appSvc = { ...svc, theme }; // Type is just Service & { theme: ... }
// Later components have no idea what's in the service
return svc.el('div')(Header(appSvc)()); // Header type is unclear
};
// ✅ CORRECT - explicit service types
type AppService = Service & {
theme: Readable<'light' | 'dark'>;
};
const App = (svc: Service) => () => {
const theme = svc.signal<'light' | 'dark'>('light');
const appSvc: AppService = { ...svc, theme };
// Header explicitly declares what it needs
return svc.el('div')(Header(appSvc)());
};
const Header = (svc: AppService) => () => {
// TypeScript knows exactly what's available
const { el, theme } = svc;
return el('header')(/* ... */);
};

Shared state should be created once and threaded through. Creating it inside component bodies means it’s recreated on each instantiation:

// ❌ WRONG - creates new signal every time component is instantiated
const ThemedSection = (svc: Service) => (props: { children: RefSpec }) => {
// This creates a NEW theme signal for every ThemedSection instance!
const theme = svc.signal<'light' | 'dark'>('light');
const themedSvc = { ...svc, theme };
return svc.el('section')(props.children);
};
// ✅ CORRECT - shared state created at app level, threaded down
const App = (svc: Service) => () => {
// Created once at the app level
const theme = svc.signal<'light' | 'dark'>('light');
const appSvc: AppService = { ...svc, theme };
return svc.el('div')(
ThemedSection(appSvc)({ children: /* ... */ })
);
};
const ThemedSection = (svc: AppService) => (props: { children: RefSpec }) => {
// Uses existing theme from service, doesn't create new one
return svc.el('section')(props.children);
};