xInjection for ReactJS - v1.0.2
    Preparing search index...

    xInjection Logo


    This is the official ReactJS implementation of the xInjection library.

    Warning

    The usage of the base library will not be explained here, I'll assume you already know how to use the xInjection library, if that's not the case, please refer to the xInjection Gettng Started section.

    First, ensure you have reflect-metadata installed:

    npm i reflect-metadata
    

    Then install xInjection for React:

    npm i @adimm/x-injection-reactjs
    
    Note

    You may also have to install the parent library via npm i @adimm/x-injection

    Add the following options to your tsconfig.json to enable decorator metadata:

    {
    "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
    }
    }
    const UserDashboardModuleBp = ProviderModule.blueprint({
    id: 'ComponentUserDashboardModule',
    imports: [UserModule],
    exports: [UserModule],
    });

    const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, () => {
    const userService = useInject(UserService);

    return (
    <h1>
    Hello {userService.firstName} {userService.lastName}!
    </h1>
    );
    });

    const App = () => {
    return (
    <>
    <Navbar />
    <UserDashboard />
    <Footer />
    </>
    );
    };

    Before continuing you should read also the Conventions section of the parent library.

    You should create a separate file which you'll use to declare the blueprint of the component module:

    user-dashboard/user-dashboard.module.ts

    export const UserDashboardModuleBp = ProviderModule.blueprint({
    id: 'ComponentUserDashboardModule',
    ...
    });
    Note

    You should also prefix the id of the blueprints with Component as this will help you to debug your app much more easier when something goes wrong.

    You should create a separate file which you'll use to declare the (main) service of the component.

    user-dashboard/user-dashboard.service.ts

    @Injectable()
    export class UserDashboardService {
    firstName: string;
    lastName: string;
    }

    You first have to either create a module or blueprint, most of the times you'll use the blueprint option, if you are asking yourself how you should decide:

    Will you have more than one instance of that component?

    • Then you have to use a blueprint, the reason can be understood by reading this.
    • Then you have to use a raw module, the reason is the opposite of the blueprint motive.
    Tip

    If the above explaination is clear, please skip to the next section, otherwise keep reading.

    Imagine that we have a Button component, clearly we'll have more than one instance of that component, this means that each instance of the Button component must have its own module, where all the singletons will act as singletons only inside the component instance.

    Therefore we leverage the blueprint import behavior to achieve that naturally without additional overhead.


    After you created the component module, you can provide it to the actual component by using the provideModuleToComponent (HoC) method.

    const UserDashboard = provideModuleToComponent(UserDashboardModuleBp, (props: UserDashboardProps) => {
    ...
    });

    If you need this design pattern, it is very easy to implement with xInjection, you actually have 2 options:

    Let's say that the Child component has this module:

    const ChildModuleBp = ProviderModule.blueprint({
    id: 'ComponentChildModule',
    providers: [ChildService],
    exports: [ChildService],
    });

    What you can now do is to provide the ChildService from the Parent component, like this:

    const ParentModuleBp = ProviderModule.blueprint({
    id: 'ComponentParentModule',
    providers: [ParentService, ChildService],
    exports: [ParentService, ChildService],
    });

    Then, when you are rendering the Child component from within the Parent component:

    const ParentComponent = provideModuleToComponent(ParentModuleBp, ({ module }) => {
    // the `module` prop is always available and automatically injected into the `props` object.

    return <ChildComponent module={module} />;
    });

    Now the ChildComponent will be instantiated with the module received from the ParentComponent, therefore it'll use the ChildService managed into the ParentModule.

    Tip

    This is perfect to use when you are writing unit tests and you want to mock an entire component module

    This is the approach which you should strive to use most of the times as it is less prone to "human error" than overriding the entire module.

    Let's re-use the same example as the one from the above, the ParentModule:

    const ParentModuleBp = ProviderModule.blueprint({
    id: 'ComponentParentModule',
    providers: [ParentService, ChildService],
    exports: [ParentService, ChildService],
    });

    And now the rendering part:

    const ParentComponent = provideModuleToComponent(ParentModuleBp, () => {
    // notice that we are not using the `module` prop anymore.
    const childService = useInject(ChildService);

    return <ChildComponent inject={[{ provide: ChildService, useValue: childService }]} />;
    });

    By using the inject prop (which as the module prop is always available) you'll "swap" the ChildService provider with a ProviderValueToken which provides the ChildService instance instantiated by the ParentComponent.

    Note

    If you are asking yourself Why would I want to do that?, that's a valid question, and most of the times you'll not need this feature, but sometimes, when you compose components, being able to control the providers of the children components becomes very useful. Check the Composable Components example to understand.

    You already have seen in action the low-level useInject hook (take a look also at the useInjectMany hook). It is quite useful when you just have to inject quickly some dependencies into a component quite simple.

    But, as your UI will grow, you'll soon discover that you may inject more dependencies into a component, or even in multiple components, therefore you'll end up writing a lot of duplicated code, well, as per the DRY principle, we want to avoid that.

    This means that we can actually use the hookFactory method to compose a custom hook with access to any dependency available in the component module.

    // The `HookWithDeps` generic type will help
    // in making sure that the `useGenerateUserFullName` hooks params are correctly visible.
    // The 1st generic param must be the hook params (Like `UserInfoProps`)
    // and the 2nd generic param must be an `array` with the providers type.
    const useGenerateUserFullName = hookFactory({
    // The `use` property is where you write your hook implementation.
    use: ({ firstName, lastName, deps: [userService] }: HookWithDeps<UserInfoProps, [UserService]>) => {
    userService.firstName = firstName;
    userService.lastName = lastName;

    return userService.generateFullName();
    },
    // The `inject` array is very important,
    // here we basically specify which dependencies should be injected into the custom hook.
    // Also, keep in mind that the order of the `inject` array matters, the order of the `deps` prop
    // is determined by the order of the `inject` array!
    inject: [UserService],
    });

    Now you can use it in inside any component which is using a module which can provide the UserService.

    export function UserInfo({ firstName, lastName }: UserInfoProps) {
    const userFullName = useGenerateFullName({ firstName, lastName });

    return <p>Hello {userFullName}!</p>;
    }

    Note: If your custom hook does not accept any parameter, you can provide void to the 1st generic type.

    e.g: use: ({ deps: [userService] }: HookWithDeps<void, [UserService]>)

    In a real world scenario, you'll definitely have custom components which render other custom components and so on... (like a Matryoshka doll)

    So you may find yourself wanting to be able to control a dependency/service of a child component from a parent component, with xInjection this is very easy to achieve thanks to the ProviderModule architecture, because each module can import and export other dependencies (or modules) it fits in perfectly within the declarative programming world!

    In this example, we'll build 4 components, each with its own purpose. However, the autocomplete component will be the one capable of accessing the services of all of them.

    • An inputbox
    • A list viewer
    • A dropdown
    • An autocomplete

    Inputbox

    inputbox.service.ts

    @Injectable()
    export class InputboxService {
    currentValue = '';

    // We'll initialize this soon enough.
    setStateValue!: (newValue: string) => void;

    /** Can be used to update the {@link currentValue} of the `inputbox`. */
    setValue(newValue: string): void {
    this.currentValue = newValue;

    this.setStateValue(this.currentValue);
    }
    }

    export const InputboxModuleBp = ProviderModule.blueprint({
    id: 'ComponentInputboxModule',
    provides: [InputboxService],
    exports: [InputboxService],
    });

    inputbox.tsx

    export interface InputboxProps {
    initialValue: string;
    }

    export const Inputbox = provideModuleToComponent<InputboxProps>(InputboxModuleBp, ({ initialValue }) => {
    const service = useInject(InputboxService);
    const [, setCurrentValue] = useState(initialValue);
    service.setStateValue = setCurrentValue;

    useEffect(() => {
    service.currentValue = initialValue;
    }, [initialValue]);

    return <input value={service.currentValue} onChange={(e) => service.setValue(e.currentTarget.value)} />;
    });

    Listview

    listview.service.ts

    @Injectable()
    export class ListviewService {
    items = [];

    /* Remaining fancy implementation */
    }

    export const ListviewModuleBp = ProviderModule.blueprint({
    id: 'ComponentListviewModule',
    provides: [ListviewService],
    exports: [ListviewService],
    });

    listview.tsx

    export interface ListviewProps {
    items: any[];
    }

    export const Listview = provideModuleToComponent<ListviewProps>(ListviewModuleBp, ({ items }) => {
    const service = useInject(ListviewService);

    /* Remaining fancy implementation */

    return (
    <div>
    {service.items.map((item) => (
    <span key={item}>{item}</span>
    ))}
    </div>
    );
    });

    Dropdown

    Now keep close attention to how we implement the Dropdown component, as it'll actually be the parent controlling the Listview component own service.

    dropdown.service.ts

    @Injectable()
    export class DropdownService {
    constructor(readonly listviewService: ListviewService) {
    // We can already take control of the children `ListviewService`!
    this.listviewService.items = [1, 2, 3, 4, 5];
    }

    /* Remaining fancy implementation */
    }

    export const DropdownModuleBp = ProviderModule.blueprint({
    id: 'ComponentDropdownModule',
    // It is very important that we import all the exportable dependencies from the `ListviewModule`!
    imports: [ListviewModuleBp],
    provides: [DropdownService],
    exports: [
    // Let's also re-export the dependencies of the `ListviewModule` so once we import the `DropdownModule`
    // somewhere elese, we get access to the `ListviewModule` exported dependencies as well!
    ListviewModuleBp,
    // Let's not forget to also export our `DropdownService` :)
    DropdownService,
    ],
    });

    dropdown.tsx

    export interface DropdownProps {
    listviewProps: ListviewProps;

    initialSelectedValue: number;
    }

    export const Dropdown = provideModuleToComponent<DropdownProps>(
    ListviewModuleBp,
    ({ listviewProps, initialSelectedValue }) => {
    const service = useInject(DropdownService);

    /* Remaining fancy implementation */

    return (
    <div className="fancy-dropdown">
    <span>{initialSelectedValue}</span>

    {/* Here we tell the `ListView` component to actually use the `ListviewService` instance we provide via the `useValue` property. */}
    {/* Each `useInject(ListviewService)` used inside the `ListView` component will automatically resolve to `service.listviewService`. */}
    <Listview {...listviewProps} inject={[{ provide: ListviewService, useValue: service.listviewService }]} />
    </div>
    );
    }
    );

    Autocomplete

    And finally the grand finale!

    autocomplete.service.ts

    @Injectable()
    export class AutocompleteService {
    constructor(
    readonly inputboxService: InputboxService,
    readonly dropdownService: DropdownService
    ) {
    // Here we can override even what the `Dropdown` has already overriden!
    this.dropdownService.listviewService.items = [29, 9, 1969];

    // However doing the following, will throw an error because the `Inputbox` component
    // at this time is not yet mounted, therefore the `setStateValue` state setter
    // method doesn't exist yet.
    //
    // A better way would be to use a store manager so you can generate your application state through
    // the services, rather than inside the UI (components should be used only to render the data, not to manipulate/manage it).
    this.inputboxService.setValue('xInjection');
    }

    /* Remaining fancy implementation */
    }

    export const AutocompleteModuleBp = ProviderModule.blueprint({
    id: 'ComponentAutocompleteModule',
    imports: [InputboxModuleBp, DropdownModuleBp],
    provides: [AutocompleteService],
    // If we don't plan to share the internal dependencies of the
    // Autocomplete component, then we can omit the `exports` array declaration.
    });

    autocomplete.tsx

    export interface AutocompleteProps {
    inputboxProps: InputboxProps;
    dropdownProps: DropdownProps;

    currentText: string;
    }

    export const Autocomplete = provideModuleToComponent<AutocompleteProps>(AutocompleteModuleBp, ({ inputboxProps, dropdownProps, currentText }) => {
    const service = useInject(AutocompleteService);

    service.inputboxService.currentValue = currentText;

    console.log(service.dropdownService.listviewService.items);
    // Produces: [29, 9, 1969]

    /* Remaining fancy implementation */

    return (
    <div className="fancy-autocomplete">
    {/* Let's not forget to replace the injection providers of both components we want to control */}
    <Inputbox {...inputboxProps} inject={[{ provide: InputboxService, useValue: service.inputboxService }]} >
    <Dropdown {...dropdownProps} inject={[{ provide: DropdownService, useValue: service.dropdownService }]} />
    </div>
    );
    }
    );

    This should cover the fundamentals of how you can build a scalable UI by using the xInjection Dependency Injection 😊

    It is very easy to create mock modules so you can provide them to your components in your unit tests.

    class ApiService {
    constructor(private readonly userService: UserService) {}

    async sendRequest<T>(location: LocationParams): Promise<T> {
    // Pseudo Implementation
    return this.sendToLocation(user, location);
    }

    private async sendToLocation(user: User, location: any): Promise<any> {}
    }

    const ApiModuleBp = new ProviderModule.blueprint({
    id: 'ApiModule',
    providers: [UserService, ApiService],
    });

    // Clone returns a `deep` clone and wraps all the `methods` to break their reference!
    const ApiModuleBpMocked = ApiModuleBp.clone().updateDefinition({
    id: 'ApiModuleMocked',
    providers: [
    {
    provide: UserService,
    useClass: UserService_Mock,
    },
    {
    provide: ApiService,
    useValue: {
    sendRequest: async (location) => {
    console.log(location);
    },
    },
    },
    ],
    });

    // Now all the dependencies used inside the "RealComponent" will be automatically resolved from the `ApiModuleBpMocked` module.
    await act(async () => render(<RealComponent module={ApiModuleBpMocked} />));

    Comprehensive, auto-generated documentation is available at:

    👉 https://adimarianmutu.github.io/x-injection-reactjs/index.html

    Pull requests are warmly welcomed! 😃

    Please ensure your contributions adhere to the project's code style. See the repository for more details.


    Note

    For questions, feature requests, or bug reports, feel free to open an issue on GitHub.