mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Change how components get dependencies injected to avoid callback nesting
This commit is contained in:
parent
4677c24242
commit
046f79270a
4 changed files with 84 additions and 36 deletions
|
@ -5,29 +5,46 @@ import { useEffect, useRef } from 'react';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
import { AppUpdateBanner } from '../common/AppUpdateBanner';
|
||||||
import { NotFound } from '../common/NotFound';
|
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 { ServersMap } from '../servers/data';
|
||||||
import type { AppSettings } from '../settings/reducers/settings';
|
import type { AppSettings } from '../settings/reducers/settings';
|
||||||
import { forceUpdate } from '../utils/helpers/sw';
|
import { forceUpdate } from '../utils/helpers/sw';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
interface AppProps {
|
type AppProps = {
|
||||||
fetchServers: () => void;
|
fetchServers: () => void;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
settings: AppSettings;
|
settings: AppSettings;
|
||||||
resetAppUpdate: () => void;
|
resetAppUpdate: () => void;
|
||||||
appUpdated: boolean;
|
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 location = useLocation();
|
||||||
const initialServers = useRef(servers);
|
const initialServers = useRef(servers);
|
||||||
const isHome = location.pathname === '/';
|
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 })}>
|
<div className={classNames('shlink-wrapper', { 'd-flex d-md-block align-items-center': isHome })}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="/settings/*" element={<SettingsComp />} />
|
<Route path="/settings/*" element={<Settings />} />
|
||||||
<Route path="/manage-servers" element={<ManageServers />} />
|
<Route path="/manage-servers" element={<ManageServers />} />
|
||||||
<Route path="/server/create" element={<CreateServer />} />
|
<Route path="/server/create" element={<CreateServer />} />
|
||||||
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
<Route path="/server/:serverId/edit" element={<EditServer />} />
|
||||||
|
@ -70,3 +87,14 @@ export const App = (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AppFactory = componentFactory(App, [
|
||||||
|
'MainHeader',
|
||||||
|
'Home',
|
||||||
|
'ShlinkWebComponentContainer',
|
||||||
|
'CreateServer',
|
||||||
|
'EditServer',
|
||||||
|
'Settings',
|
||||||
|
'ManageServers',
|
||||||
|
'ShlinkVersionsContainer',
|
||||||
|
]);
|
||||||
|
|
|
@ -1,22 +1,11 @@
|
||||||
import type Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import type { ConnectDecorator } from '../../container/types';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { App } from '../App';
|
import { AppFactory } from '../App';
|
||||||
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory(
|
bottle.factory('App', AppFactory);
|
||||||
'App',
|
|
||||||
App,
|
|
||||||
'MainHeader',
|
|
||||||
'Home',
|
|
||||||
'ShlinkWebComponentContainer',
|
|
||||||
'CreateServer',
|
|
||||||
'EditServer',
|
|
||||||
'Settings',
|
|
||||||
'ManageServers',
|
|
||||||
'ShlinkVersionsContainer',
|
|
||||||
);
|
|
||||||
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
|
bottle.decorator('App', connect(['servers', 'settings', 'appUpdated'], ['fetchServers', 'resetAppUpdate']));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
29
src/container/utils.ts
Normal file
29
src/container/utils.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,18 +2,20 @@ import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { App as createApp } from '../../src/app/App';
|
import { AppFactory } from '../../src/app/App';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
const App = createApp(
|
const App = AppFactory(
|
||||||
() => <>MainHeader</>,
|
fromPartial({
|
||||||
() => <>Home</>,
|
MainHeader: () => <>MainHeader</>,
|
||||||
() => <>ShlinkWebComponentContainer</>,
|
Home: () => <>Home</>,
|
||||||
() => <>CreateServer</>,
|
ShlinkWebComponentContainer: () => <>ShlinkWebComponentContainer</>,
|
||||||
() => <>EditServer</>,
|
CreateServer: () => <>CreateServer</>,
|
||||||
() => <>SettingsComp</>,
|
EditServer: () => <>EditServer</>,
|
||||||
() => <>ManageServers</>,
|
Settings: () => <>SettingsComp</>,
|
||||||
() => <>ShlinkVersions</>,
|
ManageServers: () => <>ManageServers</>,
|
||||||
|
ShlinkVersionsContainer: () => <>ShlinkVersions</>,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const setUp = (activeRoute = '/') => {
|
const setUp = (activeRoute = '/') => {
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
|
|
Loading…
Reference in a new issue