CellUI Substrates
The Problem with Prop Drilling
In traditional component-based frameworks, sharing state between a parent component and a deeply nested child component requires "Prop Drilling" (passing the variable down through every intermediate component).
This makes refactoring difficult and creates unnecessary boilerplate.
The CellUI Solution: Substrate
A Substrate is CellUI's elegant solution for Dependency Injection and Global State.
Think of a Substrate like a decentralized memory layer beneath your UI, like a cellular matrix. Components can "infuse" signals into the Substrate, and any other component can pull them out.
1. The Global ambient()
The easiest way to use a Substrate is via the ambient() helper. This creates a global state object that can be imported anywhere in your app.
store.ts
import { ambient } from '@cmj/cellui';
// Create a deep reactive proxy globally
export const ThemeStore = ambient('my-theme', {
mode: 'dark',
colors: {
primary: '#42a5f5'
}
});DeepChild.ts
import { ThemeStore, view } from './store';
// Any component can import it without props!
export function DeepChild() {
return view`
<div style="background: ${ThemeStore.mode}">
Primary color is: ${ThemeStore.colors.primary}
</div>
`;
}2. Isolated Contexts (Substrate)
If you are building an NPM package, or multiple CellUI apps on the same page, you don't want them sharing the global ambient scope.
For isolation, you can manually instantiate new Substrate(namespace) to scope your state.
import { Substrate, signal, view } from '@cmj/cellui';
// 1. Create a scoped container
const WidgetContext = new Substrate('my-widget');
// 2. Infuse data into the context
export function WidgetRoot() {
WidgetContext.infuse('user-id', 42);
return view`
<section>
${AnalyticsPixel()}
</section>
`;
}
// 3. Extract it without passing props
function AnalyticsPixel() {
const userId = WidgetContext.extract<number>('user-id');
return view`<div>Tracking User: ${userId}</div>`;
}