I'm trying to use InversifyJS in a react native application that uses a functional approach to components. I have everything configured and working as it should, however, the only way in which I can achieve dependency injection is through using the service locator pattern (something I want to avoid):
const Settings = (props: ISettingsProps) => {
const dispatch = useDispatch();
const navigationService = useInjection<INavigationService>(TYPES.NavigationService);
return (
<AuthenticatedView {...{ ...props, style: {justifyContent: 'flex-start', marginTop: '70%', alignItems:'center'}}}>
<Text>Settings!</Text>
<Button onPress={() => navigationService.navigate(props, "Trade", dispatch)} title="Trade" />
</AuthenticatedView>
)
}
export default Settings;
The code above uses a navigation service that is located using the aforementioned service locator.
What I would like to do, is inject the service into the component using props
however, I can't seem to work out how to do this. I have consulted several tutorials, including this one:
https://synergycodes.com/blog/dependency-injection-in-react-using-inversifyjs/
There seems to be a way if I use @lazyInject
, perhaps - but, that way of doing DI only appears to be supported in class based components (I would like to stick with functional, since react themselves recommend this particular paradigm).
I tried this:
@lazyInject(TYPES.NavigationService) let navigationService: INavigationService;
But I get the following error:
Decorators are not valid here
However, that seems like a step in the right direction (if I can get it to work).
The answer to this appears to be that custom dependency injection / IOC should be avoided. At least, this is the case when hooks are being used. The approach that I have taken, is to use the createContext
hook in order to register and consume dependencies.
First I register the dependencies in a custom AppContextWrapper component. In this case, I have two dependencies: authService and navigationService
interface IAppContext {
authService: IAuthService;
navigationService: INavigationService;
}
const contextDependencies = {
authService: new AuthService(),
navigationService: new NavigationService()
}
export const AppContext = createContext<IAppContext | null>(null);
export default function AppContextWrapper(props: IApplicationProps) {
return (
<AppContext.Provider value={contextDependencies}>
{props.children}
</AppContext.Provider>
);
}
Then wrap the main App.tsx with the wrapper:
export default function App() {
return (
<AppContextWrapper>
...
</AppContextWrapper>
)
}
I can then call the context and get the type that is required by using useContext
in the child components. Here I'm using the navigationService in my Settings screen:
const Settings = (props: ISettingsProps) => {
const dispatch = useDispatch();
const navigationService = useContext(AppContext)?.navigationService;
return (
<AuthenticatedView {...{ ...props, style: {justifyContent: 'flex-start', marginTop: '70%', alignItems:'center'}}}>
<Text>Settings!</Text>
<Button onPress={() => navigationService?.navigate(props, "Trade", dispatch)} title="Trade" />
</AuthenticatedView>
)
}
export default Settings;
This is closely related to a service locator pattern, however, there doesn't seem to be a way around that. This appears to be cleanest way of achieving DI with modern react.