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


(Spencer Elliott) #1

EDIT: I accidentally posted this twice; please go to Is it a good idea to use context for dependency injection?

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>);
    };
}

(Phips Peter) #2

I thought about this approach for Asana but we ended up sticking with our Services model. Each component declares a Services interface and has a services: Services key on Props. It results in explicit dependency injection but allows you to compose types of child components.