Search code examples
typescriptreact-nativedependency-injectioninversifyjs

How to avoid the service locator pattern when using inversify in React Native?


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).


Solution

  • 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.