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 resolveReact 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?) => thisRegister a class as singleton. The class can have a static dependencies array. Constructor args are auto-resolved. Chainable.
registerSingleton(Class, name, shortName, opts?) => thisRegister a class as singleton. The class can have a static dependencies array. Constructor args are auto-resolved. Chainable.
registerScoped(Class, name, shortName, opts?) => thisRegister a class as scoped — one instance per scope. Chainable.
registerScoped(Class, name, shortName, opts?) => thisRegister a class as scoped — one instance per scope. Chainable.
registerTransient(Class, name, shortName, opts?) => thisRegister a class as transient — new instance every resolve. Chainable.
registerTransient(Class, name, shortName, opts?) => thisRegister a class as transient — new instance every resolve. Chainable.
registerSingletonFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as singleton. Factory receives (resolver) and returns an object. Chainable.
registerSingletonFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as singleton. Factory receives (resolver) and returns an object. Chainable.
registerScopedFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as scoped. Chainable.
registerScopedFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as scoped. Chainable.
registerTransientFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as transient. Chainable.
registerTransientFactory(name, shortName, factory, deps?, opts?) => thisRegister a factory function as transient. Chainable.
registerParameterized(name, shortName, lifetime, factory, opts?) => thisRegister a parameterized factory. The factory receives (resolver, ...args) and is called with args at resolution time. Chainable.
registerParameterized(name, shortName, lifetime, factory, opts?) => thisRegister 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).
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.
resolveScoped(...serviceNames) => { $shortName: instance }Same as resolve but creates a fresh scope — scoped services get new instances.
resolveWithArgs(serviceName, ...args) => instanceResolve a parameterized service by passing arguments to its factory.
resolveWithArgs(serviceName, ...args) => instanceResolve a parameterized service by passing arguments to its factory.
createScope() => ScopeHandleCreate an isolated scope. Scoped services are cached per scope. Call dispose() to clean up.
createScope() => ScopeHandleCreate an isolated scope. Scoped services are cached per scope. Call dispose() to clean up.
clearInstances() => voidClear all cached singleton instances (except retained ones). Calls dispose() on disposable instances.
clearInstances() => voidClear all cached singleton instances (except retained ones). Calls dispose() on disposable instances.
clearService(name) => voidClear the cached instance for a specific service only.
clearService(name) => voidClear the cached instance for a specific service only.
has(name) => booleanCheck whether a service is registered under the given name.
has(name) => booleanCheck whether a service is registered under the given name.
React API
ServiceProviderReact.FC<{ container, defaultParams?, children }>Provides the container and default parameters for parameterized services. If useServiceWithArgs is called without args, these defaults are used.
ServiceProviderReact.FC<{ container, defaultParams?, children }>Provides the container and default parameters for parameterized services. If useServiceWithArgs is called without args, these defaults are used.
ServiceScopeReact.FC<{ children }>Creates an isolated scope for scoped services. Auto-disposes on unmount.
ServiceScopeReact.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.
useService(...serviceNames) => { $shortName: instance }Resolve services by name within the nearest scope (or container if no scope). Returns keyed object.
useServiceWithArgs(serviceName, ...args) => instanceResolve a parameterized service. Uses ServiceProvider defaultParams if no args passed.
useServiceWithArgs(serviceName, ...args) => instanceResolve a parameterized service. Uses ServiceProvider defaultParams if no args passed.
useScope() => ScopeHandleAccess the current scope handle directly. Must be within a ServiceScope.
useScope() => ScopeHandleAccess the current scope handle directly. Must be within a ServiceScope.
useContainer() => ServiceContainerAccess the container directly for advanced registration or resolution.
useContainer() => ServiceContainerAccess the container directly for advanced registration or resolution.
withServices(Component, config) => WrappedComponentHOC that injects resolved services as props. Config specifies service names and parameterized args.
withServices(Component, config) => WrappedComponentHOC 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.