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

55 views Asked by At

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

1

There are 1 answers

0
Alex On BEST ANSWER

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.