reactjsdependency-injectionreact-hooksinversifyjs

React with Inversify makes useEffect infinite loop


I am using CRA + TS and integrated a custom hook for dependency injection(Inversify). Here is my code:

// DI Provider
export const InversifyContext = React.createContext<{ container: Container | null }>({ container: null });

export const DiProvider: React.FC<Props> = (props) => {
  return <InversifyContext.Provider value={{ container: props.container }}>{props.children}</InversifyContext.Provider>;
};


// Custom Hook
export function useInjection<T>(identifier: interfaces.ServiceIdentifier<T>): T {
  const { container } = useContext(InversifyContext);
  if (!container) {
    throw new Error();
  }
  console.log("This is hook"); // This gets printed infinitely
  return container.get<T>(identifier);
}

// Injectable Service
@injectable()
class MyService{
  // some methods
}

// index.tsx
const container = new Container();
container.bind<MyService>("myService").to(MyService);
ReactDOM.render(
  <DiProvider container={container}>
    <MyComponent />,
  document.getElementById("root")
);

// MyComponent.tsx
const MyComponent: React.FC = () => {
  const myService = useInjection<MyService>("myService");
  useEffect(() => {
    myService.getData(); // Loop call
  }, [myService]);
}

Now when I debugged the code, I see that the provider is getting rendered infinitely, which causes the rerender of the component.


Solution

  • First, you need to understand why is this happening.

    When you use your injected service in the useEffect hook as a dependency (which you should), it triggers a component rerender, which will recall the useInjection hook and a new/updated instance of MyService is returned, because the instance is changed, the useEffect will be triggered again and this will end you up with recursive calls.

    I agree that you should not ignore the useEffect dependencies. A simple solution here would be to memoize the service within the hook.

    So your memoized hook will become:

    export function useInjection<T>(identifier: interfaces.ServiceIdentifier<T>): T {
      const { container } = useContext(InversifyContext);
      if (!container) {
        throw new Error();
      }
      console.log("This is hook"); // This gets printed infinitely
      return useMemo(() => container.get<T>(identifier), [container, identifier]);
    }
    

    Your hook will now return a memoized version of the service instance.