Shared State
Other frameworks have “context”—a way to pass values down the component tree without threading them through every intermediate component. In React, it’s createContext and useContext. In Solid, it’s similar.
Rimitive doesn’t have a separate context primitive. It doesn’t need one.
The Pattern: Service Threading
Section titled “The Pattern: Service Threading”In Rimitive, the service is the context. Components receive a service object containing the primitives 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')) );};No magic or implicit tree traversal. 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) => { ... };Why Not a Context Primitive?
Section titled “Why Not a Context Primitive?”I considered adding one. The problem: context in other frameworks works through implicit tree traversal—a component “finds” the nearest provider by walking up the tree at render time.
Rimitive components are just functions returning specs. There’s no render cycle or “currently rendering” context to hook into. Any context system would either:
- Require explicit scope passing (which is what we already have)
- Add hidden magic that fights Rimitive’s design philosophy
The service threading pattern is explicit, type-safe, and composes naturally. It’s not as terse as useContext(), but you can always see what’s happening.