Dependency Injection API#

Provide and inject dependencies throughout the component tree.

provide#

Provide a value to all descendant components.

TSX
function provide<T>(token: any, value: T): void;

Parameters#

NameTypeDescription
tokenanyUnique key (Symbol recommended)
valueTValue to provide

Examples#

TSX
import { component, provide } from 'sigx';

// Create a unique token
const ThemeToken = Symbol('theme');

const App = component(({ signal }) => {
    const theme = signal({ mode: 'dark', primary: '#007bff' });
    
    // Provide to all descendants
    provide(ThemeToken, theme);

    return () => (
        <div>
            <Header />
            <Content />
        </div>
    );
});

inject#

Inject a value provided by an ancestor component.

TSX
function inject<T>(token: any): T | undefined;

Parameters#

NameTypeDescription
tokenanyToken used in provide()

Returns#

The provided value, or undefined if not found.

Examples#

TSX
import { component, inject } from 'sigx';

const ThemeToken = Symbol('theme');

const Button = component(({ slots }) => {
    const theme = inject(ThemeToken);

    return () => (
        <button style={{ background: theme?.primary }}>
            {slots.default?.()}
        </button>
    );
});

createInjectable#

Create a self-providing injectable with automatic token management.

TSX
function createInjectable<T>(
    token: any,
    factory: () => T
): () => T;

Parameters#

NameTypeDescription
tokenanyUnique token
factory() => TFactory function

Returns#

A hook function that injects the value, creating it if needed.

Examples#

TSX
import { createInjectable, signal } from 'sigx';

// Define injectable
const useUser = createInjectable(
    Symbol('user'),
    () => signal({ name: '', loggedIn: false })
);

// Use in any component
const Profile = component(() => {
    const user = useUser();

    return () => (
        <div>
            {user.loggedIn 
                ? <span>Welcome, {user.name}</span>
                : <span>Please log in</span>
            }
        </div>
    );
});

const LoginForm = component(({ signal }) => {
    const user = useUser();
    const form = signal({ name: '' });

    const login = () => {
        user.name = form.name;
        user.loggedIn = true;
    };

    return () => (
        <form onSubmit={login}>
            <input 
                value={form.name}
                onInput={(e) => form.name = e.target.value}
            />
            <button type="submit">Login</button>
        </form>
    );
});

defineInjectable#

Create an injectable factory function. It has two forms.

Factory form — pass a factory; the returned hook lazily creates and shares one instance (a module-global singleton by default, or the provided instance when an ancestor called defineProvide).

TSX
function defineInjectable<T>(
    factory: () => T,
    options?: { name?: string },
): () => T;

Required form — pass a string name to declare a dependency that has no fallback and must be provided by an ancestor (or app.defineProvide). Injecting it unprovided throws SIGX202 — the deliberate guard against a shared singleton leaking state between requests on the server.

TSX
function defineInjectable<T>(
    name: string,
    options?: { hint?: string },
): () => T;

Options#

NameFormDescription
namefactoryA readable label for diagnostics and stack traces. Defaults to factory.name, then a definition-site pointer — set it for inline arrow factories that would otherwise be anonymous.
hintrequiredA custom message appended to the SIGX202 "not provided" error in place of the generated suggestion, e.g. { hint: 'Wrap the tree in <AuthProvider>.' }. Read only in development, but the literal still ships in your bundle unless you gate it: hint: __DEV__ ? '…' : undefined.

Returns#

A hook function that returns the injectable instance.

Server-safe singleton warning. The factory form's fallback is a module-global singleton, which can leak state across requests on a server — so in development SignalX warns when one is used on a live client. That check is gated on isLiveClient() rather than typeof window, so windowless-but-live runtimes (a Lynx background thread, a terminal app, a worker) no longer get a false warning. When you genuinely need per-app state, use the required form (or defineProvide per request) instead.

Examples#

TSX
import { defineInjectable, signal } from 'sigx';

const useCounter = defineInjectable(() => {
    const state = signal({ count: 0 });
    
    return {
        get count() { return state.count; },
        increment: () => state.count++,
        decrement: () => state.count--
    };
});

// Usage - same instance across components
const Display = component(() => {
    const counter = useCounter();
    return () => <div>Count: {counter.count}</div>;
});

const Controls = component(() => {
    const counter = useCounter();
    return () => (
        <div>
            <button onClick={counter.decrement}>-</button>
            <button onClick={counter.increment}>+</button>
        </div>
    );
});

Name an otherwise-anonymous factory, or give a required injectable a custom "not provided" hint:

TSX
// readable in diagnostics despite the inline arrow factory
const useClock = defineInjectable(() => signal(Date.now()), { name: 'clock' });

// required — no fallback; the hint replaces the generated SIGX202 suggestion
const useSession = defineInjectable<Session>('Session', {
    hint: 'Provide it at the root: app.defineProvide(useSession, () => loadSession()).',
});

defineProvide#

Provide an injectable and return its instance.

TSX
function defineProvide<T>(useFn: () => T): T;

Parameters#

NameTypeDescription
useFn() => TInjectable created by defineInjectable

Returns#

The created instance (also provided to descendants).

Examples#

TSX
import { component, defineInjectable, defineProvide } from 'sigx';

const useStore = defineInjectable(() => {
    const state = signal({ items: [] });
    return {
        get items() { return state.items; },
        add: (item) => state.items = [...state.items, item]
    };
});

// Root component creates and provides
const App = component(() => {
    const store = defineProvide(useStore);
    
    // Can use store here
    store.add('Initial item');

    return () => (
        <div>
            <ItemList />
            <AddItemForm />
        </div>
    );
});

// Children inject it
const ItemList = component(() => {
    const store = useStore();
    return () => (
        <ul>
            {store.items.map(item => <li>{item}</li>)}
        </ul>
    );
});

injectApp#

Inject the application instance.

TSX
function injectApp(): App | undefined;

Returns#

The current app instance, or undefined if not in an app context.

Examples#

TSX
import { component, injectApp } from 'sigx';

const PluginUser = component(() => {
    const app = injectApp();
    
    // Access app-level functionality
    const router = app?.router;
    const config = app?.config;

    return () => <div>...</div>;
});

Patterns#

Service Pattern#

TSX
// services/auth.ts
import { defineInjectable, signal } from 'sigx';

export const useAuth = defineInjectable(() => {
    const state = signal({ 
        user: null as User | null,
        loading: false 
    });

    return {
        get user() { return state.user; },
        get isAuthenticated() { return !!state.user; },
        get loading() { return state.loading; },
        
        login: async (email: string, password: string) => {
            state.loading = true;
            try {
                state.user = await authApi.login(email, password);
            } finally {
                state.loading = false;
            }
        },
        
        logout: () => {
            state.user = null;
        }
    };
});

Config Pattern#

TSX
// Provide configuration at app root
const ConfigToken = Symbol('config');

const App = component(() => {
    provide(ConfigToken, {
        apiUrl: 'https://api.example.com',
        debug: true
    });
    
    return () => <Router />;
});

// Inject anywhere
const ApiClient = component(() => {
    const config = inject(ConfigToken);
    // Use config.apiUrl
});

Theme Pattern#

TSX
const ThemeToken = Symbol('theme');

const useTheme = createInjectable(ThemeToken, () => 
    signal({ 
        mode: 'light' as 'light' | 'dark',
        colors: { primary: '#007bff' }
    })
);

const ThemeProvider = component(({ slots }) => {
    const theme = useTheme();
    
    return () => (
        <div class={`theme-${theme.mode}`}>
            {slots.default?.()}
        </div>
    );
});

const ThemeToggle = component(() => {
    const theme = useTheme();
    
    const toggle = () => {
        theme.mode = theme.mode === 'light' ? 'dark' : 'light';
    };
    
    return () => (
        <button onClick={toggle}>
            {theme.mode === 'light' ? '🌙' : '☀️'}
        </button>
    );
});

Types#

InjectionKey#

For type-safe injection, use typed tokens:

TSX
import type { InjectionKey } from 'sigx';

interface User {
    id: number;
    name: string;
}

const UserToken: InjectionKey<User> = Symbol('user');

// Type-safe provide/inject
provide(UserToken, { id: 1, name: 'John' });
const user = inject(UserToken); // User | undefined