Reactive DI
Dependency injection with reactivity unobtrusive state-management in mobx manner, applied to react-like components, css-in-js. Compatible with flow, react, but free from framework lock-in (no React.createElement, Inferno.createVNode), etc. Size about 10kb reactive-di.min.js + 11kb lom_atom.min.js
example source, demo, todomvc benchmark
Install
npm install --save reactive-di lom_atom babel-plugin-transform-metadata
Example .babelrc:
{
"presets": [
"flow",
"react",
["es2015", {"loose": true}]
],
"plugins": [
"transform-metadata",
"transform-decorators-legacy",
["transform-react-jsx", {"pragma": "lom_h"}]
]
}
babel-plugin-transform-metadata is optional, used for metadata generation.
Debug
Build rdi and copy to ../app-project/node_modules/reactive-di
npm run watch --reactive-di:dest=../app-project
Hello world
Setup:
import {mem, action} from 'lom_atom'
import {createReactWrapper, createCreateElement, Injector} from 'reactive-di'
import {render, h, Component} from 'preact'
function ErrorableView({error}: {error: Error}) {
return <div>
{error instanceof mem.Wait
? <div>
Loading...
</div>
: <div>
<h3>Fatal error !</h3>
<div>{error.message}</div>
<pre>
{error.stack.toString()}
</pre>
</div>
}
</div>
}
const lomCreateElement = createCreateElement(
createReactWrapper(
Component,
ErrorableView
),
h
)
global['lom_h'] = lomCreateElement
Usage:
class HelloContext {
@mem name = ''
}
function HelloView(
{prefix}: {prefix: string},
{context}: {context: HelloContext}
) {
return <div>
{prefix}, {context.name}
<br/><input value={context.name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
</div>
}
render(<HelloView prefix="Hello" />, document.body)
Features
Typesafe context in components
function HelloView(
{prefix}: {prefix: string},
{context}: {context: HelloContext}
) {
return <div>...</div>
}
Or
function HelloView(
_: {},
context: HelloComponent
) {
}
Context signature generated from component via babel-plugin-transform-metadata.
Classes as keys. Without plugin, we need to define metadata manually:
HelloView.deps = [{context: HelloContext}]
Injector in createElement (lom_h) automatically initializes HelloContext and pass it to HelloView in
render(<HelloView prefix="Hello" />, document.body)
State management based on lom_atom
Rdi based on lom_atom, state management library, like mobx, but much simpler and with some killer features. Statefull or stateless components in rdi - pure functions.
Modifying state:
import {action, mem} from 'lom_atom'
class HelloContext {
@mem name = ''
}
function HelloView(
_: {},
{context}: {context: HelloContext}
) {
return <input value={context.name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
}
All state changes are asynchronous, but for prevent loosing cursor position in react input, action helper used.
Asynchronous code
Loading actual state:
import {mem, force} from 'lom_atom'
class HelloContext {
@force force: HelloContext
@mem set name(next: string | Error) {}
@mem get name(): string {
fetch('/hello')
.then((r: Response) => r.json())
.then((data: Object) => {
this.name = data.name
})
.catch((e: Error) => {
this.name = e
})
throw new mem.Wait()
}
}
function HelloView(
_: {},
context: HelloContext
) {
return <div>
<input value={context.name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
<button onClick={() => { context.forced.name }}>Reload from server</button>
</div>
}
First time context.name
invokes fetch('/hello') and actualizes state, second time - context.name
returns value from cache.
context.forced.name
invokes fetch handler again.
context.name = value
value sets directly into cache.
context.forced.name = value
invokes set name handler in HelloContext and sets into cache.
Error handling
class HelloContext {
@mem get name() {
throw new Error('oops')
}
}
function HelloView(
_: {},
{context}: {context: HelloContext}
) {
return <input value={context.name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
}
Accessing context.name
throws oops, try/catch in HelloView wrapper displays default ErrorableView, registered in rdi:
function ErrorableView({error}: {error: Error}) {
return <div>
{error instanceof mem.Wait
? <div>
Loading...
</div>
: <div>
<h3>Fatal error !</h3>
<div>{error.message}</div>
<pre>
{error.stack.toString()}
</pre>
</div>
}
</div>
}
const lomCreateElement = createCreateElement(
createReactWrapper(
Component,
ErrorableView
),
h
)
global['lom_h'] = lomCreateElement
We can manually handle error:
function HelloView(
_: {},
{context}: {context: HelloContext}
) {
let name: string
try {
name = context.name
} catch (e) {
name = 'Error:' + e.message
}
return <input value={name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
}
Loading status handing
Looks like error handling. throw new mem.Wait()
throws some specific exception.
class HelloContext {
@force force: HelloContext
@mem set name(next: string | Error) {}
@mem get name(): string {
fetch('/hello')
.then((r: Response) => r.json())
.then((data: Object) => {
this.name = data.name
})
.catch((e: Error) => {
this.name = e
})
throw new mem.Wait()
}
}
Catched in HelloComponent wrapper and default ErrorableView shows loader instead of HelloView.
function ErrorableView({error}: {error: Error}) {
return <div>
{error instanceof mem.Wait
? <div>
Loading...
</div>
: <div>
<h3>Fatal error !</h3>
<div>{error.message}</div>
<pre>
{error.stack.toString()}
</pre>
</div>
}
</div>
}
We can manually define loader in component, using try/catch:
function HelloView(
_: {},
{context}: {context: HelloContext}
) {
let name: string
try {
name = context.name
} catch (e) {
if (e instanceof mem.Wait) { name = 'Loading...' }
else { throw e }
}
return <input value={name} onInput={
action((e: Event) => {
context.name = (e.target: any).value
})
} />
}
Registering default dependencies
class SomeAbstract {}
class SomeConcrete extends SomeAbstract {}
class C {
a: SomeAbstract
constructor(a: SomeAbstract) {
this.a = a
}
}
const injector = new Injector(
[
[SomeAbstract, new SomeConcrete()]
]
)
injector.value(SomeAbstract).a instanceof SomeConcrete
Components cloning
Creates slightly modified component.
import {cloneComponent} from 'reactive-di'
class FirstCounterService {
@mem value = 0
}
function CounterMessageView({value}: {value: string}) {
return <div>count: {value}</div>
}
function FirstCounterView(
_: {},
counter: FirstCounterService
) {
return <div>
<CounterMessageView value={counter.value}/>
<button id="FirstCounterAddButton" onClick={() => { counter.value++ }}>Add</button>
</div>
}
class SecondCounterService {
@mem value = 1
}
const SecondCounterView = cloneComponent(FirstCounterView, [
[FirstCounterService, SecondCounterService],
['FirstCounterAddButton', null],
], 'SecondCounterView')
Hierarchical dependency injection
class SharedService {}
function Parent(
props: {},
context: {sharedService: SharedService}
) {
return <Child parentService={context.sharedService} />
}
function Child(
context: {sharedService: SharedService}
) {
}
class SharedService {}
function Parent() {
return <Child/>
}
function Child(
props: {},
context: {sharedService: SharedService}
) {
}
Optional css-in-js support
Via adapters rdi supports css-in-js with reactivity and dependency injection power:
Setup:
import {mem} from 'lom_atom'
import {createReactWrapper, createCreateElement, Injector} from 'reactive-di'
import {h, Component} from 'preact'
import {create as createJss} from 'jss'
import ErrorableView from './ErrorableView'
const jss = createJss()
const defaultDeps = []
const injector = new Injector(defaultDeps, jss)
const lomCreateElement = createCreateElement(
createReactWrapper(
Component,
ErrorableView,
injector
),
h
)
global['lom_h'] = lomCreateElement
Usage:
import {mem} from 'lom_atom'
import type {NamesOf} from 'lom_atom'
class ThemeVars {
@mem color = 'red'
}
class MyTheme {
vars: ThemeVars
constructor(vars: ThemeVars) {
this._vars = vars
}
@mem @theme get css() {
return {
wrapper: {
backgroundColor: this._vars.color
}
}
}
}
function MyView(
props: {},
{theme: {css}, vars}: {theme: MyTheme, vars: ThemeVars}
) {
return <div class={css.wrapper}>...
<button onClick={() => vars.color = 'green'}>Change color</button>
</div>
}
Styles automatically mounts/unmounts together with component. Changing vars.color
automatically rebuilds and remounts css.
Passing component props to its depenendencies
Sometimes we need to pass component properties to its services.
import {mem} from 'lom_atom'
import {props} from 'reactive-di'
interface MyProps {
some: string;
}
class MyViewService {
@props _props: MyProps;
@mem get some(): string {
return this._props.some + '-suffix'
}
}
function MyView(
props: MyProps,
{service}: {service: MyViewService}
) {
return <div>{service.some}</div>
}
React compatible
We still can use any react/preact/inferno components together with rdi components.
Logging
import {defaultContext, BaseLogger} from 'lom_atom'
import type {ILogger} from 'lom_atom'
class Logger extends BaseLogger {
create<V>(host: Object, field: string, key?: mixed): V | void {}
destroy(atom: IAtom<*>): void {}
status(status: ILoggerStatus, atom: IAtom<*>): void {}
error<V>(atom: IAtom<V>, err: Error): void {}
newValue<V>(atom: IAtom<V>, from?: V | Error, to: V, isActualize?: boolean): void {}
}
defaultContext.setLogger(new Logger())
Map config to objects
Configs maped to object properties by class names.
import {mem} from 'lom_atom'
import {Injector} from 'reactive-di'
const defaultDeps = []
const injector = new Injector([], undefined, {
SomeService: {
name: 'test',
id: 123
}
})
class SomeService {
static displayName = 'SomeService'
@mem name = ''
id = 0
}
const someService: SomeService = injector.value(SomeService)
someService.name === 'test'
someService.id === 123
babel-plugin-transform-metadata can generate displayName. To enable it, add ["transform-metadata", {"addDisplayName": true}]
into .babelrc.
Example .babelrc:
{
"presets": [
"flow",
"react",
["es2015", {"loose": true}]
],
"plugins": [
["transform-metadata", {"addDisplayName": true}],
"transform-decorators-legacy",
["transform-react-jsx", {"pragma": "lom_h"}]
]
}
Credits