Is it a good idea to use context for dependency injection?


(Spencer Elliott) #1

My team and I are trying to figure out a good structured approach to inject dependencies into react components in TypeScript, and we’ve currently settled on an approach using context, i.e.:

interface ITestProps {
    text: string;
}

interface ITestDependencies {
    setting: boolean;
}

// The "presentational" component
const TestViaContext = (props: ITestProps, context: IDependenciesContext<ITestDependencies>) => {
    return <div data-setting={ context.dependencies.setting }>{ props.text }</div>;
}

// The resolved "container" component providing dependency values
const ResolvedTestViaContext = getResolvedStatelessComponent(TestViaContext, {
    setting: true
});

const elementViaContext = <ResolvedTestViaContext text='test' />;

The reason for using context is we get a clean separation of the “presentational” props and the “dependency” props, the getResolvedStatelessComponent higher-order component can infer the ITestDependencies type, and the presentational component doesn’t need to be concerned with the possibility that context values can change between renders. (Whereas with props, the component should account for the possibility that e.g. props.dependencies.setting will change between renders.)

But my concern with using context is that it feels like we’re not using it for its intended purpose, which is to pass values down to deep descendants in the component tree. We’re sort of hacking context by calling the stateless component directly and passing in context values, rather than using React.createElement. (See getResolvedStatelessComponent / getResolvedStatelessComponentViaProps implementation below.)

I’m thinking that injecting dependencies directly through props would be a better approach:

// The "presentational" component
const TestViaProps: IResolvableStatelessComponentViaProps<ITestProps, ITestDependencies> = (props) => {
    return <div data-setting={ props.dependencies.setting }>{ props.text }</div>;
}

// The resolved "container" component providing dependency values
const ResolvedTestViaProps = getResolvedStatelessComponentViaProps(TestViaProps, {
    setting: true
});

const elementViaProps = <ResolvedTestViaProps text='test' />;

This is similar to how react-redux’s connect() HoC works with its mapStateToProps and mapDispatchToProps options.

Has anyone here implemented a similar dependency injection approach? Interested to hear people’s thoughts.

getResolvedStatelessComponent / getResolvedStatelessComponentViaProps implementation
export interface IResolvableStatelessComponent<TProps, TDependencies> {
    (props: TProps, context?: IDependenciesContext<TDependencies>): React.ReactElement<any>;

    /**
     * A resolvable component must declare a 'dependencies' context property, or else it will never receive the dependencies.
     */
    contextTypes?: IDependenciesContextTypes;
}

export function getResolvedStatelessComponent<TProps, TDependencies>(statelessComponent: IResolvableStatelessComponent<TProps, TDependencies>, dependencies: TDependencies): React.StatelessComponent<TProps> {
    const {
        contextTypes: {
            dependencies: unused = undefined,
        ...contextTypes
        } = {}
    } = statelessComponent;

    const resolvedComponent: React.StatelessComponent<TProps> = (props: TProps, context: any) => {
        return statelessComponent(props, {
            ...context,
            dependencies: dependencies
        });
    };

    resolvedComponent.contextTypes = contextTypes;

    return resolvedComponent;
}

export interface IDependenciesProps<TDependencies> {
    dependencies: TDependencies;
}

export interface IResolvableStatelessComponentViaProps<TProps, TDependencies> {
    (props: TProps & IDependenciesProps<TDependencies>): React.ReactElement<any>;
}

export function getResolvedStatelessComponentViaProps<TProps, TDependencies>(statelessComponent: IResolvableStatelessComponentViaProps<TProps, TDependencies>, dependencies: TDependencies): React.StatelessComponent<TProps> {
    return (props: TProps) => {
        return statelessComponent({
            ...(props || {}),
            dependencies: dependencies
        } as TProps & IDependenciesProps<TDependencies>);
    };
}

Is it a good idea to use context for dependency injection?
(Andy Edwards) #2

In my opinion injecting mocks for testing via context is generally fine, but the way you’re doing it in that code example is too complicated. Why not just pass down setting by doing something like the following (sorry I’ve omitted type information, I know Flow well but not TypeScript):

class SettingContainer extends React.Component {
  static childContextTypes = {
    setting: PropTypes.bool.isRequired,
  }
  getChildContext() {
    return {setting: this.props.setting}
  }
  render() { return this.props.children }
}

And create an HOC with a name that’s easy to understand and just injects the setting from context into the wrapped component’s props:

function injectSetting(WrappedComponent) {
  return class SettingInjector extends React.Component {
    static contextTypes = {
      setting: PropTypes.bool.isRequired,
    }
    render() {
      return <WrappedComponent {...this.props} setting={this.context.setting} />
    }
  }
}

const TestViaContext = injectSetting(
  props => <div data-setting={props.setting}>{props.text}</div>
)

const elementViaContext = <TestViaContext text="test" />

const appInTest = (
  <SettingContainer setting={true}>
    <TestViaContext text="test" />
  </SettingContainer>
)

Putting setting within a “dependencies” prop on context is totally redundant. Think about it: anything a component needs from props or context is a dependency.