Fluxo UIFluxo UIv0.4.1

Dependency Injection

A class-based dependency injection container. Register service classes with static dependency arrays, swap implementations by environment, and resolve by name — the container handles instantiation and wiring.

Basic Usage

Register classes with injectable(), declare dependencies via static arrays, and resolve by name. The container instantiates the class and injects its dependencies as constructor arguments.

Class-Based Registration

import { createContainer } from 'fluxo-ui/services';

const services = createContainer();

// Class with static dependencies — constructor args are auto-resolved
class LoggerService {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
}

class UserRepository {
    static dependencies = ['LoggerService'];

    constructor(private $logger: LoggerService) {}

    findById(id: string) {
        this.$logger.log(`Finding user ${id}`);
        return { id, name: 'John Doe' };
    }
}

// Register classes — chained
services
    .registerSingleton(LoggerService, 'LoggerService', '$logger')
    .registerSingleton(UserRepository, 'UserRepository', '$userRepo');

// Resolve by name — container instantiates and injects dependencies
const { $userRepo } = services.resolve('UserRepository');
$userRepo.findById('123'); // Logs: [LOG] Finding user 123

// Resolve multiple at once
const { $logger, $userRepo: repo } = services.resolve('LoggerService', 'UserRepository');

With Interfaces

// With interfaces — register different implementations under the same name
interface ILogger {
    log(message: string): void;
    warn(message: string): void;
}

class ConsoleLogger implements ILogger {
    log(message: string) { console.log(message); }
    warn(message: string) { console.warn(message); }
}

class SilentLogger implements ILogger {
    log(_message: string) { /* noop */ }
    warn(_message: string) { /* noop */ }
}

// Register whichever implementation you need
services.registerSingleton<ILogger>(
    import.meta.env.PROD ? SilentLogger : ConsoleLogger,
    'LoggerService',
    '$logger'
);

// Consumer just uses the interface — doesn't know which class is behind it
const { $logger } = services.resolve<{ $logger: ILogger }>('LoggerService');
$logger.log('works with any implementation');

Factory-Based Registration

// Factory-based — no class needed, just return an object
services.registerSingletonFactory<ILogger>(
    'LoggerService',
    '$logger',
    () => ({
        log: (message: string) => console.log(`[APP] ${message}`),
        warn: (message: string) => console.warn(`[APP] ${message}`),
    })
);

// Factory with dependencies — resolver gives access to other services
services.registerSingletonFactory(
    'NotificationService',
    '$notification',
    (resolver) => {
        const logger = resolver.resolve<ILogger>('LoggerService');
        return {
            notify(message: string) {
                logger.log(`Notification: ${message}`);
            },
        };
    },
    ['LoggerService'] // declare deps for circular detection
);

const { $notification } = services.resolve('NotificationService');
$notification.notify('Hello!'); // Logs: [APP] Notification: Hello!

Service Lifetimes

Control instance caching: singleton (default, one instance forever), scoped (one per scope), transient (always new), and retain (survives clearInstances).

import { createContainer } from 'fluxo-ui/services';

const services = createContainer();

class CounterService {
    private value = 0;
    increment() { return ++this.value; }
    getValue() { return this.value; }
}

// SINGLETON (default) — same instance every time
services.registerSingleton(CounterService, 'CounterService', '$counter');

const { $counter: a } = services.resolve('CounterService');
const { $counter: b } = services.resolve('CounterService');
a.increment(); // 1
b.increment(); // 2 — same instance
console.log(a === b); // true


// TRANSIENT — new instance every time
services.registerTransient(CounterService, 'CounterService', '$counter');

const { $counter: c } = services.resolve('CounterService');
const { $counter: d } = services.resolve('CounterService');
c.increment(); // 1
d.increment(); // 1 — different instances
console.log(c === d); // false


// SCOPED — one instance per scope, different across scopes
services.registerScoped(CounterService, 'CounterService', '$counter');

const scope1 = services.createScope();
const scope2 = services.createScope();

const e = scope1.resolve<CounterService>('CounterService');
const f = scope1.resolve<CounterService>('CounterService');
const g = scope2.resolve<CounterService>('CounterService');

e.increment(); // 1
f.increment(); // 2 — same scope, same instance
g.increment(); // 1 — different scope, different instance

scope1.dispose();
scope2.dispose();


// Factory-based works with all lifetimes too
services.registerScopedFactory(
    'RequestContext', '$reqCtx',
    () => ({ requestId: crypto.randomUUID(), startedAt: Date.now() })
);


// RETAIN — singleton that survives clearInstances()
services.registerSingleton(CounterService, 'CounterService', '$counter', { retain: true });

Parameterized Services

Register factories that receive arguments at resolution time. The factory also receives a resolver to access other services. Arguments become part of the cache key for singleton and scoped lifetimes.

import { createContainer } from 'fluxo-ui/services';

const services = createContainer();

// A singleton auth service
class AuthService {
    getTokenForUser(userId: string) {
        return `bearer-token-for-${userId}`;
    }
}

services.registerSingleton(AuthService, 'AuthService', '$auth');

// Parameterized — userId is passed at resolution time
// The factory receives (resolver, ...args)
services.registerParameterized(
    'ApiClient', '$api', 'scoped',
    (resolver, userId: string) => {
        const auth = resolver.resolve<AuthService>('AuthService');
        const token = auth.getTokenForUser(userId);

        return {
            userId,
            get: async (path: string) => {
                console.log(`GET ${path} as ${userId} with ${token}`);
                return { data: 'response' };
            },
        };
    }
);

// Resolve with arguments
const scope = services.createScope();
const client = scope.resolveWithArgs('ApiClient', 'user-42');
await client.get('/api/profile');
// Logs: GET /api/profile as user-42 with bearer-token-for-user-42

// Same args in same scope → same instance (scoped lifetime)
const client2 = scope.resolveWithArgs('ApiClient', 'user-42');
console.log(client === client2); // true

// Different args → different instance
const client3 = scope.resolveWithArgs('ApiClient', 'user-99');
console.log(client === client3); // false

scope.dispose();

Swapping Implementations

Register different classes under the same service name based on environment, config, or runtime conditions. Consumer code resolves by name and never knows which implementation it gets.

Swapping Classes

import { createContainer } from 'fluxo-ui/services';

const services = createContainer();

// Define interface
interface IBrowserService {
    openUrl(url: string): void;
    getStorage(key: string): string | null;
}

// Different class implementations
class ChromeBrowserService implements IBrowserService {
    openUrl(url: string) { chrome.tabs.create({ url }); }
    getStorage(key: string) { return localStorage.getItem(key); }
}

class FirefoxBrowserService implements IBrowserService {
    openUrl(url: string) { browser.tabs.create({ url }); }
    getStorage(key: string) { return localStorage.getItem(key); }
}

class DevBrowserService implements IBrowserService {
    openUrl(url: string) { console.log('DEV: would open', url); }
    getStorage(key: string) { return sessionStorage.getItem(key); }
}

// Register the right one based on environment
const browserType = detectBrowser();

if (browserType === 'chrome') {
    services.registerSingleton<IBrowserService>(ChromeBrowserService, 'BrowserService', '$browser');
} else if (browserType === 'firefox') {
    services.registerSingleton<IBrowserService>(FirefoxBrowserService, 'BrowserService', '$browser');
} else {
    services.registerSingleton<IBrowserService>(DevBrowserService, 'BrowserService', '$browser');
}

// Consumer code doesn't care which implementation
class NotificationService {
    static dependencies = ['BrowserService'];
    constructor(private $browser: IBrowserService) {}

    notify(message: string, link?: string) {
        console.log(message);
        if (link) this.$browser.openUrl(link);
    }
}

services.registerSingleton(NotificationService, 'NotificationService', '$notification');
const { $notification } = services.resolve('NotificationService');
$notification.notify('New PR!', 'https://github.com/...');

Swapping with Factories

// Same pattern works with factories
if (import.meta.env.PROD) {
    services.registerSingletonFactory<IBrowserService>(
        'BrowserService', '$browser',
        () => new ChromeBrowserService()
    );
} else {
    services.registerSingletonFactory<IBrowserService>(
        'BrowserService', '$browser',
        () => ({
            openUrl: (url: string) => console.log('Mock open:', url),
            getStorage: (key: string) => sessionStorage.getItem(key),
        })
    );
}

// Re-registering clears all non-retained singleton caches automatically
services.registerSingleton<IBrowserService>(DevBrowserService, 'BrowserService', '$browser');
// ^ All services that depended on BrowserService will get fresh instances next resolve

React Integration

ServiceProvider supplies default parameters for parameterized services. ServiceScope creates isolated scopes that auto-dispose on unmount. Hooks and HOC for consuming services in components.

Provider Setup

import { createContainer, ServiceProvider, ServiceScope } from 'fluxo-ui/services';

const services = createContainer();

// Class-based
class ThemeService {
    primary = '#2563eb';
    toggle() { /* ... */ }
}

services.registerSingleton(ThemeService, 'ThemeService', '$theme');

// Factory-based
services.registerTransientFactory(
    'ApiService', '$api',
    () => ({
        fetch: (url: string) => window.fetch(url).then(r => r.json()),
    })
);

// Parameterized
services.registerParameterized(
    'UserApiService', '$userApi', 'scoped',
    (resolver, userId: string) => {
        const api = resolver.resolve('ApiService');
        return {
            getProfile: () => api.fetch(`/api/users/${userId}`),
            getActivity: () => api.fetch(`/api/users/${userId}/activity`),
        };
    }
);

// App root — defaultParams provides fallback args for parameterized services
function App() {
    const userId = useCurrentUserId();

    return (
        <ServiceProvider container={services} defaultParams={{ UserApiService: [userId] }}>
            <Dashboard />
            <ServiceScope>
                <UserPanel />
            </ServiceScope>
        </ServiceProvider>
    );
}

Hooks

import { useService, useServiceWithArgs, useContainer } from 'fluxo-ui/services';

// useService — resolve by name, get { $shortName: instance }
function Dashboard() {
    const { $theme, $api } = useService('ThemeService', 'ApiService');

    return (
        <div style={{ color: $theme.primary }}>
            <button onClick={() => $api.fetch('/api/stats')}>Load Stats</button>
        </div>
    );
}

// useServiceWithArgs — uses defaultParams from ServiceProvider if no args passed
function UserPanel() {
    const userApi = useServiceWithArgs('UserApiService');

    // Or override with explicit args
    const adminApi = useServiceWithArgs('UserApiService', 'admin-user-id');

    return (
        <div>
            <button onClick={() => userApi.getProfile()}>My Profile</button>
            <button onClick={() => adminApi.getActivity()}>Admin Activity</button>
        </div>
    );
}

// useContainer — direct access to the container
function AdminPanel() {
    const container = useContainer();
    const resetAll = () => container.clearInstances();
    return <button onClick={resetAll}>Reset Services</button>;
}

Higher-Order Component

import { withServices } from 'fluxo-ui/services';

function ProfileComponent({ $theme, $api, username }) {
    return (
        <div style={{ color: $theme.primary }}>
            <h2>{username}</h2>
            <button onClick={() => $api.fetch('/profile')}>Refresh</button>
        </div>
    );
}

const Profile = withServices(ProfileComponent, {
    services: ['ThemeService', 'ApiService'],
});

// Usage — only pass non-injected props
<Profile username="Jane Doe" />

Scopes

import { ServiceScope, useScope } from 'fluxo-ui/services';

// ServiceScope auto-creates and disposes a scope on mount/unmount
function RequestHandler() {
    return (
        <ServiceScope>
            <RequestContent />
        </ServiceScope>
    );
}

function RequestContent() {
    const scope = useScope();

    // All scoped services resolved here share the same scope
    const db = scope.resolve('DbConnection');
    const logger = scope.resolve('LoggerService');

    // Scope auto-disposes on unmount
    return <div>...</div>;
}

Import

import {
    createContainer,
    ServiceContainer,
    ServiceProvider,
    ServiceScope,
    useService,
    useServiceWithArgs,
    useScope,
    useContainer,
    withServices,
} from 'fluxo-ui/services';

// Create a container
const services = createContainer();

// Class-based registration (chainable)
services
    .registerSingleton(MyClass, 'MyService', '$myService')
    .registerScoped(OtherClass, 'OtherService', '$other')
    .registerTransient(AnotherClass, 'AnotherService', '$another');

// Factory-based registration (chainable)
services
    .registerSingletonFactory('MyService', '$myService', (resolver) => ({ ... }))
    .registerScopedFactory('OtherService', '$other', (resolver) => ({ ... }))
    .registerTransientFactory('AnotherService', '$another', (resolver) => ({ ... }));

// Parameterized (chainable)
services.registerParameterized('UserApi', '$userApi', 'scoped', (resolver, userId) => ({ ... }));

Container API

registerSingleton
(Class, name, shortName, opts?) => this

Register a class as singleton. The class can have a static dependencies array. Constructor args are auto-resolved. Chainable.

registerScoped
(Class, name, shortName, opts?) => this

Register a class as scoped — one instance per scope. Chainable.

registerTransient
(Class, name, shortName, opts?) => this

Register a class as transient — new instance every resolve. Chainable.

registerSingletonFactory
(name, shortName, factory, deps?, opts?) => this

Register a factory function as singleton. Factory receives (resolver) and returns an object. Chainable.

registerScopedFactory
(name, shortName, factory, deps?, opts?) => this

Register a factory function as scoped. Chainable.

registerTransientFactory
(name, shortName, factory, deps?, opts?) => this

Register a factory function as transient. Chainable.

registerParameterized
(name, shortName, lifetime, factory, opts?) => this

Register a parameterized factory. The factory receives (resolver, ...args) and is called with args at resolution time. Chainable.

resolve
(...serviceNames) => { $shortName: instance }

Resolve one or more services by name. Returns an object keyed by short names (e.g. $dashboard, $session).

resolveScoped
(...serviceNames) => { $shortName: instance }

Same as resolve but creates a fresh scope — scoped services get new instances.

resolveWithArgs
(serviceName, ...args) => instance

Resolve a parameterized service by passing arguments to its factory.

createScope
() => ScopeHandle

Create an isolated scope. Scoped services are cached per scope. Call dispose() to clean up.

clearInstances
() => void

Clear all cached singleton instances (except retained ones). Calls dispose() on disposable instances.

clearService
(name) => void

Clear the cached instance for a specific service only.

has
(name) => boolean

Check whether a service is registered under the given name.

React API

ServiceProvider
React.FC<{ container, defaultParams?, children }>

Provides the container and default parameters for parameterized services. If useServiceWithArgs is called without args, these defaults are used.

ServiceScope
React.FC<{ children }>

Creates an isolated scope for scoped services. Auto-disposes on unmount.

useService
(...serviceNames) => { $shortName: instance }

Resolve services by name within the nearest scope (or container if no scope). Returns keyed object.

useServiceWithArgs
(serviceName, ...args) => instance

Resolve a parameterized service. Uses ServiceProvider defaultParams if no args passed.

useScope
() => ScopeHandle

Access the current scope handle directly. Must be within a ServiceScope.

useContainer
() => ServiceContainer

Access the container directly for advanced registration or resolution.

withServices
(Component, config) => WrappedComponent

HOC that injects resolved services as props. Config specifies service names and parameterized args.

Features

Class & Factory Registration

Register service classes with static dependency arrays, or plain factory functions returning objects. Both support interface typing.

3 Lifetimes

Singleton (one instance), scoped (per-scope), and transient (always new) lifetime management.

Parameterized Factories

Register factories that accept arguments at resolution time for dynamic, user-scoped service creation.

Implementation Swapping

Register different classes or factories under the same service name to swap implementations by environment or config.

Circular Dependency Detection

Detects circular dependencies at registration time and throws descriptive errors with the full dependency chain.

React Integration

ServiceProvider with default params, ServiceScope for isolation, useService hook, useContainer, and withServices HOC.