Lazy DI and Cyclic DI

Sometimes, you may need to make two stores depend on each other. This may sound impossible for some other DI system but in Deviation, this is possible:

@Inject({
    todoStore: TodoStore
})
export class CounterStore extends Store {
    get todoStore() {
        return this.props.todoStore
    }
    
    state = {
        count: 0
    }
}

@Inject({
    counterStore: CounterStore
})
export class TodoStore extends Store {
    get counterStore() {
        return this.props.counterStore
    }

    state = {
        todos: []
    }
}

As you might see, this won't work. Because both CounterStore and TodoStore are using decorator, so the classes are not hoisted. Which results in undefined when TodoStore is called:

@Inject({
    todoStore: TodoStore // undefined
})
export class CounterStore extends Store { }

So, we will fix this by making Dependency Injection become lazy. Change the code to the following will fix the problem:

@Inject({
    todoStore: () => TodoStore
})
export class CounterStore extends Store { }

@Inject({
    counterStore: () => CounterStore
})
export class TodoStore extends Store { }

Caveats

Although Cyclic DI is powerful, there still some caveats that you have to notice. The following example is evil and won't work in Deviation:

@Inject({
    todoStore: () => TodoStore
})
export class CounterStore extends Store {
    state = {
        count: this.props.todoStore.state.todos.length
    }
}

@Inject({
    counterStore: () => CounterStore
})
export class TodoStore extends Store {
    state = {
        todos: Array.from({ length: this.props.counterStore.state.count })
    }
}

To understand the mechanism of how Cyclic DI works in Deviation. We need to take a look at where we define these stores in our app:

ReactDOM.render(
    <Deviation providers={[CounterStore, TodoStore]}>
        <AppRoot />
    </Deviation>,
    document.getElementById('root')
)

In this example, CounterStore is declared first, then the CounterStore will be initialized first. TodoStore will be initialized then. That means TodoStore is unavailable to CounterStore until TodoStore, but CounterStore will be available to TodoStore. So you may ask, what if we want to access TodoStore from CounterStore when TodoStore have just been initialized. We can do that by using storeDidMount:

export class CounterStore extends Store {
    get todoStore() {
        this.props.todoStore
    }

    storeDidMount() {
        this.setState({
            count: this.todoStore.state.todos.length
        })
    }
}

Last updated