DEV Community

Guillermo Pérez Farquharson
Guillermo Pérez Farquharson

Posted on

React Dependency Injection

Hey. I'm not that used to testing. But I do care about dependency injection. Because I understand that decoupling is a good thing.

So, consider these two cases:

Funcional component that depends on imports:

import React from 'react';
import serviceA from './serviceA';
import serviceB from './serviceB';

function MyComponent() {
  // ...
  serviceA();
  serviceB();

  return <></>;
}

Class component that depends on imports:

import React from 'react';
import serviceA from './serviceA';
import serviceB from './serviceB';

class MyComponent extends React.Component() {

  componentDidMount() {
    serviceA();
    serviceB();    
  }

  // ...

  render() {
    return <></>;
  }
}

A best approach would be to just pass all dependencies as props. Right?

import React from 'react';

function MyComponent({serviceA, serviceB}) {
  // ...
  serviceA();
  serviceB();

  return <></>;
}

But still, I want to have defaults to my services. I mean, I want my components to resolve their own dependencies when possible. So... back to dirty imports.

import serviceA from './serviceA';
import serviceB from './serviceB';

Apart from defaults, sometimes you just don't want certain dependencies to come from the parent component. Because they are implementation details for a given task. For example:

import React, {Component, Fragment} from 'react';
import { isSingleWord } from "../helpers/selection";

class TextEditor extends Component { ... }

I wouldn't expect isSingleWord to be injected as a prop. Not everything is meant to be a prop.

However, unit testing should test just one thing, and isSingleWord should have it's own separated test. While TextEditor would be tested by mocking the isSingleWord dependency.

Test libraries like Jest let you change the import path of your dependencies on the fly. But that's too magical for me. What could be done then?

Well, working with functions, I use to set dependencies as default parameters, like this:

import React from 'react';
import serviceA from './serviceA';
import serviceB from './serviceB';

function MyComponent({
  dependencyA: serviceA,
  dependencyB: serviceB,
}) {
  // ...
  dependencyA();
  dependencyB();

  return <></>;
}

That works with functional components just fine.

What about classes? I guess I could try the same trick in the constructor:


class MyComponent extends React.Component() {

  constructor(props) {
    super(props);

    const {dependencyA, dependencyB} = props;

    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;    
  }

  //...
}

But that's too verbose, and maybe not that expressive. So, I'd propose to use a HOC. Like:

import React from 'react';
import serviceA from './serviceA';
import serviceB from './serviceB';
import withDependencyInjection from './helpers/withDependencyInjection';

class MyComponent extends React.Component() {

  componentDidMount() {
    const {dependencyA, dependencyB} = this.props;

    dependencyA();
    dependencyB();    
  }

  // ...

  render() {
    return <></>;
  }
}

const dependencies = {
  dependencyA: serviceA,
  dependencyB: serviceB
};

export default withDependencyInjection(MyComponent, dependencies);

export { TestableComponent: MyComponent }; // you'd inject props yourself in test

So, with this HOC you now have defaults for your props in classes.

import React from 'react';

function withDependencyInjection(WrappedComponent, dependencies){
  return function(props) {
    return (
      <WrappedComponent
        {...props}
        {...dependencies}
      />
    )
  }
}

export default withDependencyInjection;

Let me know your thoughts.

¿Do you import dependencies directly in your components?
¿Do you pass them as props instead?
And ¿do you set defaults for services when they come as props?

For testing, I guess the tendency to mock imports on the fly with Jest is really the way to go. I'm just sharing my thoughts.

Top comments (0)