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 statetype 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.
Nested Overrides
Section titled “Nested Overrides”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)() );};With Portable Components
Section titled “With Portable Components”The portable component pattern ((svc) => (props) => spec) works naturally with this:
// Portable component that expects theme in serviceconst 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.
TypeScript Tips
Section titled “TypeScript Tips”Define your extended service types explicitly:
import type { Service } from './service';import type { Readable } from '@rimitive/signals/types';
// Base service with your extensionsexport type AppService = Service & { theme: Readable<'light' | 'dark'>; user: Readable<User | null>; // add more as needed};
// For components that only need a subsetexport 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 serviceconst UserProfile = (svc: AppService) => () => { ... };Anti-patterns
Section titled “Anti-patterns”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 singletonconst 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 explicitlyconst 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.
Don’t Mutate the Service Object
Section titled “Don’t Mutate the Service Object”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 objectconst DarkSection = (svc: AppService) => () => { svc.theme = svc.signal('dark'); // Mutates the original! return svc.el('section')(/* ... */);};// ✅ CORRECT - create a new extended serviceconst 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 safetyconst 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 typestype 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')(/* ... */);};Don’t Create Shared State Inside Render
Section titled “Don’t Create Shared State Inside Render”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 instantiatedconst 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 downconst 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);};