@portento/core
v0.2.0
Published
Lightweight dependency injection framework for React with MobX integration
Maintainers
Readme
Portento Core
Lightweight dependency injection framework for React and React Native with seamless MobX integration. Build scalable applications with clean architecture using decorators and IoC patterns.
Features
- 🎯 Three-tier DI scoping - Root, Router, and Component-level dependency management
- ⚡ MobX Integration - Automatic observer wrapping for reactive components
- 🎨 Decorator-based API - Clean, declarative syntax with
@Injectable,@Component,@Router - 🔄 Automatic Resolution - Smart dependency injection with hierarchical fallback
- 🧪 Testing Utilities - Reset scopes for isolated unit tests
- 📦 TypeScript-first - Full type safety and IntelliSense support
- 🌐 Framework Agnostic - Works with React, React Native, and Angular
Installation
yarn add @portento/core react mobx mobx-react tsyringe reflect-metadataOr with npm:
npm install @portento/core react mobx mobx-react tsyringe reflect-metadataTypeScript Configuration
Enable decorators in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true
}
}Import reflect-metadata at your app entry point:
import 'reflect-metadata';
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('MyApp', () => App);Quick Start
1. Create a Service with @Injectable
import { Injectable } from '@portento/core';
import { makeAutoObservable } from 'mobx';
@Injectable({ providedIn: 'root' })
class AuthService {
public isAuthenticated = false;
constructor() {
makeAutoObservable(this);
}
login(username: string) {
this.isAuthenticated = true;
}
logout() {
this.isAuthenticated = false;
}
}2. Create a Component with @Component
import React from 'react';
import { View, Text, Button } from 'react-native';
import { Component, IoComponent } from '@portento/core';
@Component({
selector: 'home-screen',
providers: []
})
class HomeScreen implements IoComponent {
constructor(
private authService: AuthService
) {}
render() {
return (
<View>
<Text>
{this.authService.isAuthenticated ? 'Logged In' : 'Not Logged In'}
</Text>
<Button
title="Login"
onPress={() => this.authService.login('user')}
/>
</View>
);
}
}
export const HomeScreenComponent = Component.$provider(HomeScreen);3. Use in Your App
import { HomeScreenComponent } from './HomeScreen';
export default function App() {
return <HomeScreenComponent />;
}Dependency Injection Scoping
Portento Core provides a three-tier scoping hierarchy for dependency management:
1. Root Scope (Singleton)
Services registered at root scope are singletons shared across the entire application:
@Injectable({ providedIn: 'root' })
class UserStore {
public username = 'Guest';
constructor() {
makeAutoObservable(this);
}
}2. Router Scope (Feature-level)
Services shared across components within the same router:
@Injectable()
class SharedNavStore {
public currentTab = 0;
constructor() {
makeAutoObservable(this);
}
}
@Router({
selector: 'main-nav-router',
components: [HomeScreen, ProfileScreen],
providers: [SharedNavStore]
})
class MainNavRouter implements IoComponent {
render() {
return (
<View>
<HomeScreen.$provider />
<ProfileScreen.$provider />
</View>
);
}
}3. Component Scope (Local)
Services isolated to a single component instance:
@Injectable()
class FormValidator {
public errors: string[] = [];
validate(value: string) {
// Validation logic
}
}
@Component({
selector: 'login-form',
providers: [FormValidator]
})
class LoginForm implements IoComponent {
constructor(private validator: FormValidator) {}
render() {
// Component implementation
}
}Resolution Hierarchy
Dependencies are resolved with automatic fallback:
Component Scope → Router Scope → Root ScopeIf a dependency isn't found in the component's providers, it searches the router's providers, then falls back to root scope.
MobX Integration
All components are automatically wrapped with MobX's observer() HOC for reactive updates:
@Injectable({ providedIn: 'root' })
class CounterStore {
public count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++; // Component automatically re-renders
}
}
@Component({
selector: 'counter',
providers: []
})
class Counter implements IoComponent {
constructor(private store: CounterStore) {}
render() {
return (
<View>
<Text>{this.store.count}</Text>
<Button title="+" onPress={() => this.store.increment()} />
</View>
);
}
}Component Export Patterns
Pattern 1: Direct Decoration
@Component({
selector: 'home-screen',
providers: []
})
class HomeScreen implements IoComponent {
render() {
return <View>...</View>;
}
}
// Usage in JSX:
<HomeScreen.$provider />Pattern 2: Separate Class & Export (Recommended)
@Component({
selector: 'settings-screen',
providers: []
})
class Settings implements IoComponent {
render() {
return <View>...</View>;
}
}
export const SettingsScreen = Component.$provider(Settings);
// Usage in JSX (cleaner):
<SettingsScreen />Lifecycle Methods
Components support standard React lifecycle methods:
@Component({
selector: 'my-component',
providers: []
})
class MyComponent implements IoComponent {
componentDidMount() {
console.log('Component mounted');
}
componentDidUpdate(prevProps, prevState) {
console.log('Component updated');
}
componentWillUnmount() {
console.log('Component unmounting');
}
render() {
return <View>...</View>;
}
}Testing Utilities
Reset dependency instances for isolated unit tests:
import { resetScope, resetClass, resetAll } from '@portento/core';
// Reset all root scope instances
resetScope('root');
// Reset specific router scope
resetScope('router', 'main-nav-router');
// Reset component scope
resetScope('component', 'home-screen');
// Reset specific class (conceptually)
resetClass('AuthService');
// Reset everything
resetAll();API Reference
@Injectable(params)
Register a class as an injectable service.
Parameters:
providedIn?: 'root'- Register as root-scoped singleton
@Injectable({ providedIn: 'root' })
class MyService {}@Component(params)
Create a React component with dependency injection.
Parameters:
selector: string- Unique component identifierproviders?: Array<Class>- Component-scoped services
@Component({
selector: 'my-component',
providers: [LocalService]
})
class MyComponent implements IoComponent {}@Router(params)
Create a router component that groups child components with shared dependencies.
Parameters:
selector: string- Unique router identifiercomponents: Array<Class>- Child componentsproviders?: Array<Class>- Router-scoped services
@Router({
selector: 'main-router',
components: [ScreenA, ScreenB],
providers: [SharedStore]
})
class MainRouter implements IoComponent {}IoComponent Interface
Base interface for all components:
interface IoComponent<Props = any, State = any> {
state?: State;
componentDidMount?(): void;
componentDidUpdate?(prevProps: Props, prevState: State): void;
componentWillUnmount?(): void;
render(): React.ReactNode;
}Controller Type
Access React component state and methods:
interface Controller<Props = any, State = any> {
props: Props;
state: State;
setState: (state: Partial<State> | ((prevState: State) => State)) => void;
forceUpdate: () => void;
}Inject the controller:
@Component({
selector: 'stateful-component',
providers: []
})
class StatefulComponent implements IoComponent {
constructor(private controller: Controller) {}
updateState() {
this.controller.setState({ counter: 1 });
}
}Examples
Complete usage examples are available in the @portento/core-examples package:
yarn add @portento/core-examplesExamples include:
- StoreExample - MobX observable stores with different scopes
- ScopingExample - Dependency injection hierarchy demonstration
- RouterScopeExample - Shared state across router components
- ResetExample - Cleanup utilities for testing
Troubleshooting
Decorators not working
Ensure experimentalDecorators and emitDecoratorMetadata are enabled in tsconfig.json.
"Design:paramtypes" metadata missing
Import reflect-metadata at your app entry point before any other imports.
MobX observables not triggering re-renders
Make sure your stores use makeAutoObservable(this) in the constructor.
Dependency not found
Check the resolution order: component providers → router providers → root scope. Ensure the service is registered at the appropriate level.
Architecture Documentation
For detailed architecture information, see:
- USAGE_PATTERNS.md - Component patterns
License
MIT © Luca Leone
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
