Do not inject components into other components

This commit is contained in:
Alejandro Celaya
2025-11-15 11:46:19 +01:00
parent dad3990c23
commit d10bea50bc
17 changed files with 156 additions and 197 deletions

View File

@@ -1,24 +0,0 @@
import type { IContainer } from 'bottlejs';
import { createContext, useContext } from 'react';
const ContainerContext = createContext<IContainer | null>(null);
export const ContainerProvider = ContainerContext.Provider;
export const useDependencies = <T extends unknown[]>(...names: string[]): T => {
const container = useContext(ContainerContext);
if (!container) {
throw new Error('You cannot use "useDependencies" outside of a ContainerProvider');
}
return names.map((name) => {
const dependency = container[name];
if (!dependency) {
throw new Error(`Dependency with name "${name}" not found in container`);
}
return dependency;
}) as T;
};
// TODO Create Higher Order Component that can pull dependencies from the container

60
src/container/context.tsx Normal file
View File

@@ -0,0 +1,60 @@
import type { IContainer } from 'bottlejs';
import { type ComponentType, createContext, useContext } from 'react';
const ContainerContext = createContext<IContainer | null>(null);
export const ContainerProvider = ContainerContext.Provider;
const useContainer = (wrapperName: string): IContainer => {
const container = useContext(ContainerContext);
if (!container) {
throw new Error(`You cannot use "${wrapperName}" outside of a ContainerProvider`);
}
return container;
};
/**
* Hook used to extract dependencies from the container in other hooks.
*/
export const useDependencies = <T extends unknown[]>(...names: string[]): T => {
const container = useContainer('useDependencies');
return names.map((name) => {
const dependency = container[name];
if (!dependency) {
throw new Error(`Dependency with name "${name}" not found in container`);
}
return dependency;
}) as T;
};
/**
* Higher Order Component used to inject services into components as props.
*/
export function withDependencies<
Props extends Record<string, unknown>,
DependencyName extends string & keyof Props,
>(
Component: ComponentType<Props>,
dependencyNames: DependencyName[],
): ComponentType<Omit<Props, DependencyName>> {
function Wrapper(props: Omit<Props, DependencyName>) {
const container = useContainer('withDependencies');
// Inject services, unless they have been overridden by props passed from
// the parent component.
const dependencies: Partial<Record<DependencyName, unknown>> = {};
for (const dependency of dependencyNames) {
if (!(dependency in props)) {
dependencies[dependency] = container[dependency];
}
}
const propsWithServices = { ...dependencies, ...props } as Props;
return <Component {...propsWithServices} />;
}
return Wrapper;
}

View File

@@ -2,13 +2,6 @@ import { useTimeoutToggle } from '@shlinkio/shlink-frontend-kit';
import { FetchHttpClient } from '@shlinkio/shlink-js-sdk/fetch';
import Bottle from 'bottlejs';
import { buildShlinkApiClient } from '../api/services/ShlinkApiClientBuilder';
import { AppFactory } from '../app/App';
import { Home } from '../common/Home';
import { ShlinkWebComponentContainerFactory } from '../common/ShlinkWebComponentContainer';
import { CreateServerFactory } from '../servers/CreateServer';
import { ImportServersBtnFactory } from '../servers/helpers/ImportServersBtn';
import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer';
import { ManageServersFactory } from '../servers/ManageServers';
import { ServersExporter } from '../servers/services/ServersExporter';
import { ServersImporter } from '../servers/services/ServersImporter';
import { csvToJson, jsonToCsv } from '../utils/helpers/csvjson';
@@ -35,22 +28,5 @@ bottle.serviceFactory('useTimeoutToggle', () => useTimeoutToggle);
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'HttpClient');
// Components
bottle.factory('App', AppFactory);
bottle.serviceFactory('Home', () => Home);
bottle.decorator('Home', withoutSelectedServer);
bottle.factory('ShlinkWebComponentContainer', ShlinkWebComponentContainerFactory);
bottle.factory('ManageServers', ManageServersFactory);
bottle.decorator('ManageServers', withoutSelectedServer);
bottle.factory('CreateServer', CreateServerFactory);
bottle.decorator('CreateServer', withoutSelectedServer);
bottle.factory('ImportServersBtn', ImportServersBtnFactory);
// Services
bottle.service('ServersImporter', ServersImporter, 'csvToJson');
bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'jsonToCsv');

View File

@@ -1,27 +0,0 @@
import type { IContainer } from 'bottlejs';
import type { FC } from 'react';
import { useMemo } from 'react';
export type FCWithDeps<Props, Deps> = FC<Props> & Partial<Deps>;
export function useDependencies<Deps>(obj: Deps): Omit<Required<Deps>, keyof FC> {
return useMemo(() => obj as Omit<Required<Deps>, keyof FC>, [obj]);
}
export function componentFactory<Deps, CompType = Omit<Partial<Deps>, keyof FC>>(
Component: CompType,
deps: ReadonlyArray<keyof CompType>,
) {
return (container: IContainer, console = globalThis.console) => {
deps.forEach((dep) => {
const resolvedDependency = container[dep as string];
if (!resolvedDependency && process.env.NODE_ENV !== 'production') {
console.error(`[Debug] Could not find "${dep as string}" dependency in container`);
}
Component[dep] = resolvedDependency;
});
return Component;
};
}