Change how components get dependencies injected to avoid callback nesting

This commit is contained in:
Alejandro Celaya 2023-09-03 11:06:35 +02:00
parent 4677c24242
commit 046f79270a
4 changed files with 84 additions and 36 deletions

View file

@ -5,29 +5,46 @@ import { useEffect, useRef } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { NotFound } from '../common/NotFound';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServersMap } from '../servers/data';
import type { AppSettings } from '../settings/reducers/settings';
import { forceUpdate } from '../utils/helpers/sw';
import './App.scss';
interface AppProps {
type AppProps = {
fetchServers: () => void;
servers: ServersMap;
settings: AppSettings;
resetAppUpdate: () => void;
appUpdated: boolean;
}
};
type AppDependencies = {
MainHeader: FC;
Home: FC;
ShlinkWebComponentContainer: FC;
CreateServer: FC;
EditServer: FC;
Settings: FC;
ManageServers: FC;
ShlinkVersionsContainer: FC;
};
const App: FCWithDeps<AppProps, AppDependencies> = (
{ fetchServers, servers, settings, appUpdated, resetAppUpdate },
) => {
const {
MainHeader,
Home,
ShlinkWebComponentContainer,
CreateServer,
EditServer,
Settings,
ManageServers,
ShlinkVersionsContainer,
} = useDependencies(App);
export const App = (
MainHeader: FC,
Home: FC,
ShlinkWebComponentContainer: FC,
CreateServer: FC,
EditServer: FC,
SettingsComp: FC,
ManageServers: FC,
ShlinkVersionsContainer: FC,
) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => {
const location = useLocation();
const initialServers = useRef(servers);
const isHome = location.pathname === '/';
@ -52,7 +69,7 @@ export const App = (
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
<Routes>
<Route index element={<Home />} />
<Route path="/settings/*" element={<SettingsComp />} />
<Route path="/settings/*" element={<Settings />} />
<Route path="/manage-servers" element={<ManageServers />} />
<Route path="/server/create" element={<CreateServer />} />
<Route path="/server/:serverId/edit" element={<EditServer />} />
@ -70,3 +87,14 @@ export const App = (
</div>
);
};
export const AppFactory = componentFactory(App, [
'MainHeader',
'Home',
'ShlinkWebComponentContainer',
'CreateServer',
'EditServer',
'Settings',
'ManageServers',
'ShlinkVersionsContainer',
]);

View file

@ -1,22 +1,11 @@
import type Bottle from 'bottlejs';
import type { ConnectDecorator } from '../../container/types';
import { App } from '../App';
import { AppFactory } from '../App';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory(
'App',
App,
'MainHeader',
'Home',
'ShlinkWebComponentContainer',
'CreateServer',
'EditServer',
'Settings',
'ManageServers',
'ShlinkVersionsContainer',
);
bottle.factory('App', AppFactory);
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
// Actions

29
src/container/utils.ts Normal file
View file

@ -0,0 +1,29 @@
import type { IContainer } from 'bottlejs';
import type { FC } from 'react';
import { useRef } from 'react';
export type FCWithDeps<Props, Deps> = FC<Props> & Partial<Deps>;
export function useDependencies<Deps>(obj: Deps): Omit<Required<Deps>, keyof FC> {
const depsRef = useRef(obj as Omit<Required<Deps>, keyof FC>);
return depsRef.current;
}
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`);
}
// eslint-disable-next-line no-param-reassign
Component[dep] = resolvedDependency;
});
return Component;
};
}

View file

@ -2,18 +2,20 @@ import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { App as createApp } from '../../src/app/App';
import { AppFactory } from '../../src/app/App';
describe('<App />', () => {
const App = createApp(
() => <>MainHeader</>,
() => <>Home</>,
() => <>ShlinkWebComponentContainer</>,
() => <>CreateServer</>,
() => <>EditServer</>,
() => <>SettingsComp</>,
() => <>ManageServers</>,
() => <>ShlinkVersions</>,
const App = AppFactory(
fromPartial({
MainHeader: () => <>MainHeader</>,
Home: () => <>Home</>,
ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer</>,
CreateServer: () => <>CreateServer</>,
EditServer: () => <>EditServer</>,
Settings: () => <>SettingsComp</>,
ManageServers: () => <>ManageServers</>,
ShlinkVersionsContainer: () => <>ShlinkVersions</>,
}),
);
const setUp = (activeRoute = '/') => {
const history = createMemoryHistory();