Closed misantronic closed 10 months ago
@misantronic There are plans for implementing injection with hooks. :smiley:
Here is a little idea I had for using tsdi with hooks
const tsdi = new TSDI();
tsdi.enableComponentScanner();
function useTSDI<T = {}>(Store: { new (): T } | string, scope?: string): T {
if (scope) {
useTSDIScope(scope);
}
return React.useMemo(() => tsdi.get(Store), [Store]);
}
function useTSDIScope(scope: string): void {
const tsdiScope = tsdi.getScope(scope);
React.useEffect(() => {
return () => tsdiScope.leave();
}, []);
React.useState(tsdiScope.enter());
}
function MyComponent() {
const store = useTSDI(MyStore);
...
}
@misantronic That would be a first way but it requires to create the TSDI instance to be in the scope of the hook function. For a proof of concept it will be good enough. From an API perspective I would not tie the scope to the getter of an injection. This will not cover all use cases of scopes. I would suggest something like this:
function useTSDI<T>(dependency: { new (): T }): T {
@external class Injector {
@inject tsdi!: TSDI;
public get(): T { return this.tsdi.get(dependency); }
}
return new Injector().get();
}
function useScope(scope: string): void {
@external class Injector {
@inject tsdi!: TSDI;
public scope(): T { tsdi.getScope(scope); }
}
const injector = new Injector();
React.useEffect(() => {
injector.scope().enter();
return () => injector.scope().leave();
}, []);
}
Not sure if this is good enough right now. But you can see this as prototypical implementation and use it.
Great proposal! I actually have some concerns in practice, entering the scope during the effect. that way you cannot write synchronous code when depending on a scope:
const store = useTSDI(StoreWithScope);
useScope('scope');
store.myMethod(); // will not work
So the scope would have to be entered beforehand:
...
React.useEffect(() => {
return () => injector.scope().leave();
}, []);
injector.scope().enter();
export function useTSDI<T>(Dependency: { new (): T }): T {
return useMemo(
() => {
@external
class Injector {
@inject tsdi!: TSDI;
public get(): T {
return this.tsdi.get(Dependency);
}
}
return new Injector().get();
},
[Dependency]
);
}
export function useScope(scope: string): void {
const tsdiScope = useMemo(
() => {
@external
class Injector {
@inject private tsdi!: TSDI;
public scope(): {
enter(): void;
leave(): void;
} {
return this.tsdi.getScope(scope);
}
}
const injector = new Injector();
const injectorScope = injector.scope();
injectorScope.enter();
return injectorScope;
},
[scope]
);
useEffect(() => {
return () => tsdiScope.leave();
}, []);
}
@misantronic I'm not sure what you mean by write synchronous code when depending on a scope
. The call order should be like this:
useScope('scope');
const store = useTSDI(StoreWithScope);
store.myMethod(); // will not work
I've worked with hooks until now but the useEffect
hook should run while mounting a component and therefore I guess it will be executed right away. But that is an implmentation detail, it would work also by directly calling the enter
method in a custom hook. It my result in problems in the future, since then you call enter
on every render. If the implementation of scopes is changed to use e.g. reference counts this will break, because multiple calls to enter will only have one leave call during unmount.
Therefore I recommended a different approach.
Another question I have is: Why do you use React.memo
here? There is nothing to memorize. The hard work is done internally by TSDI and memorizing will result in strange side-effects for example if you have a non-singleton component in TSDI. Since the class/prototype you give to useTSDI
is still the same you will get a stale component instead of a fresh one.
As a baseline you should not cache TSDI managed instances.
@misantronic Here is a fully working example with react, tsdi and mobx:
import { observable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { render } from "react-dom";
import { component, external, inject, TSDI } from "tsdi";
@component
class State {
@observable
public counter = 0;
}
const tsdi = new TSDI();
tsdi.enableComponentScanner();
function useTSDI<T>(dependency: { new (): T }): T {
@external
class Injector {
@inject tsdi!: TSDI;
public get(): T {
return this.tsdi.get(dependency);
}
}
return new Injector().get();
}
const App = observer(function App() {
const state = useTSDI(State);
return (
<div>
Hello World! {state.counter}
<button onClick={() => state.counter++}>Click +</button>
<button onClick={() => state.counter--}>Click -</button>
</div>
);
});
render(<App />, document.getElementById("app"));
Besides the working example above, I've noticed that mobx observer
is not compatible with hooks since it creates a HOC as class component instead of a function component. Therefore in that case you may either need a HOC for the TSDI scope or you need something different for state management which can trigger rerenders. That may be useState
but you probaby will have to deal with deep state changes (like above).
And as well this will not work:
useScope('scope');
const store = useTSDI(StoreWithScope);
store.myMethod(); // will not work
I have not check this but it may be, because the useEffect
hooks mimics the componentDidMount
and not the componentWillMount
lifecycle.
And in the code above the assumption was that the effect is called synchronous on the first render (which would be close to componentWillMount
).
So I guess you probably need some HOC to enter and leave the scope.
Besides the working example above, I've noticed that mobx
observer
is not compatible with hooks since it creates a HOC as class component instead of a function component. Therefore in that case you may either need a HOC for the TSDI scope or you need something different for state management which can trigger rerenders. That may beuseState
but you probaby will have to deal with deep state changes (like above).
So there is a lib mobx-react-lite which fully supports react-hooks. works great
Some day, react hooks will take over and some people might try to live without using classes. Will tsdi make it in this world?