Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e9951e9
Set everything up to use hooks for reduc actions and state
acelaya Nov 14, 2025
b295240
Stop injecting dependencies in selectServer action
acelaya Nov 14, 2025
7890d00
Create useSelectedServer hook and use it where reset selected server …
acelaya Nov 14, 2025
9c1052c
Replace usage of injected selectedServer with useSelectedServer
acelaya Nov 14, 2025
11bbef3
Create dedicated store module
acelaya Nov 14, 2025
145765e
Enable immutable and serializable redux checks
acelaya Nov 14, 2025
ae7aea0
Infer redux types when possible
acelaya Nov 14, 2025
a7f2d32
Do not inject servers state or actions
acelaya Nov 14, 2025
9e8498b
Do not inject remoteServers state or actions
acelaya Nov 14, 2025
6094994
Do not inject settings state or actions
acelaya Nov 14, 2025
d566920
Bump typescript-eslint from 8.46.3 to 8.46.4 in the eslint group
dependabot[bot] Nov 15, 2025
74e3b8f
Bump @shlinkio/shlink-frontend-kit in the shlink group
dependabot[bot] Nov 15, 2025
e0e0b24
Bump node from 25.1-alpine to 25.2-alpine
dependabot[bot] Nov 15, 2025
b5f77de
Bump @vitejs/plugin-react from 5.1.0 to 5.1.1 in the vite group
dependabot[bot] Nov 15, 2025
4abaa5b
Bump the vitest group with 4 updates
dependabot[bot] Nov 15, 2025
8492d47
Bump react-router from 7.9.5 to 7.9.6
dependabot[bot] Nov 15, 2025
79d8e3d
Merge pull request #1738 from shlinkio/dependabot/npm_and_yarn/react-…
acelaya Nov 15, 2025
c379709
Merge pull request #1737 from shlinkio/dependabot/npm_and_yarn/vitest…
acelaya Nov 15, 2025
fb25745
Merge pull request #1736 from shlinkio/dependabot/npm_and_yarn/vite-d…
acelaya Nov 15, 2025
88092fc
Merge pull request #1734 from shlinkio/dependabot/docker/node-25.2-al…
acelaya Nov 15, 2025
491ebd3
Merge pull request #1732 from shlinkio/dependabot/npm_and_yarn/eslint…
acelaya Nov 15, 2025
728b608
Merge pull request #1733 from shlinkio/dependabot/npm_and_yarn/shlink…
acelaya Nov 15, 2025
af23fa8
Bump js-yaml from 4.1.0 to 4.1.1
dependabot[bot] Nov 15, 2025
fced67d
Bump the react group with 2 updates
dependabot[bot] Nov 15, 2025
e7b18ed
Merge pull request #1739 from shlinkio/dependabot/npm_and_yarn/js-yam…
acelaya Nov 15, 2025
73a8855
Merge pull request #1735 from shlinkio/dependabot/npm_and_yarn/react-…
acelaya Nov 15, 2025
f301513
Expose container via provider
acelaya Nov 15, 2025
373f0db
Do not inject appupdated state or actions
acelaya Nov 15, 2025
4b65576
Consolidate all service definitions in one module
acelaya Nov 15, 2025
b6f1db5
Add context test
acelaya Nov 15, 2025
dad3990
Merge pull request #1731 from acelaya-forks/redux-hooks
acelaya Nov 15, 2025
d10bea5
Do not inject components into other components
acelaya Nov 15, 2025
119d909
Merge pull request #1740 from acelaya-forks/remove-component-di
acelaya Nov 15, 2025
9080dde
Add v4.6.1 to changelog
acelaya Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).

## [4.6.1] - 2025-11-15
### Added
* *Nothing*

### Changed
* [#802](https://github.com/shlinkio/shlink-web-client/issues/802) Improve dependency injection in components.
* Stop injecting redux state and actions.

### Deprecated
* *Nothing*

### Removed
* *Nothing*

### Fixed
* Fix small UI issues.


## [4.6.0] - 2025-11-12
### Added
* [shlink-web-component#839](https://github.com/shlinkio/shlink-web-component/issues/839) Allow filtering short URLs by excluded tags when using Shlink >=4.6.0
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:25.1-alpine AS node
FROM node:25.2-alpine AS node
COPY . /shlink-web-client
ARG VERSION="latest"
ENV VERSION=${VERSION}
Expand Down
660 changes: 329 additions & 331 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
"@json2csv/plainjs": "^7.0.6",
"@reduxjs/toolkit": "^2.10.1",
"@shlinkio/data-manipulation": "^1.0.4",
"@shlinkio/shlink-frontend-kit": "^1.3.0",
"@shlinkio/shlink-frontend-kit": "^1.3.1",
"@shlinkio/shlink-js-sdk": "^3.0.1",
"@shlinkio/shlink-web-component": "^0.17.0",
"@vitest/browser-playwright": "^4.0.8",
"@vitest/browser-playwright": "^4.0.9",
"bottlejs": "^2.0.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.1",
Expand All @@ -42,7 +42,7 @@
"react-dom": "^19.2.0",
"react-external-link": "^2.6.1",
"react-redux": "^9.2.0",
"react-router": "^7.9.5",
"react-router": "^7.9.6",
"redux-localstorage-simple": "^2.5.1",
"workbox-core": "^7.3.0",
"workbox-expiration": "^7.3.0",
Expand All @@ -58,11 +58,11 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@total-typescript/shoehorn": "^0.1.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/browser": "^4.0.3",
"@vitest/coverage-v8": "^4.0.8",
"@vitest/coverage-v8": "^4.0.9",
"adm-zip": "^0.5.16",
"axe-core": "^4.11.0",
"chalk": "^5.6.2",
Expand All @@ -77,7 +77,7 @@
"playwright": "^1.56.1",
"tailwindcss": "^4.1.3",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.2",
"vite-plugin-pwa": "^1.1.0",
"vitest": "^4.0.3"
Expand Down
7 changes: 3 additions & 4 deletions src/api/services/ShlinkApiClientBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { HttpClient } from '@shlinkio/shlink-js-sdk';
import { ShlinkApiClient } from '@shlinkio/shlink-js-sdk';
import type { GetState } from '../../container/types';
import type { ServerWithId } from '../../servers/data';
import { hasServerData } from '../../servers/data';
import type { GetState } from '../../store';

const apiClients: Map<string, ShlinkApiClient> = new Map();

const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
typeof getStateOrSelectedServer === 'function';
const getSelectedServerFromState = (getState: GetState): ServerWithId => {
const { selectedServer } = getState();
if (!hasServerData(selectedServer)) {
Expand All @@ -18,7 +16,7 @@ const getSelectedServerFromState = (getState: GetState): ServerWithId => {
};

export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelectedServer: GetState | ServerWithId) => {
const { url: baseUrl, apiKey, forwardCredentials } = isGetState(getStateOrSelectedServer)
const { url: baseUrl, apiKey, forwardCredentials } = typeof getStateOrSelectedServer === 'function'
? getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer;
const serverKey = `${apiKey}_${baseUrl}_${forwardCredentials ? 'forward' : 'no-forward'}`;
Expand All @@ -34,6 +32,7 @@ export const buildShlinkApiClient = (httpClient: HttpClient) => (getStateOrSelec
{ requestCredentials: forwardCredentials ? 'include' : undefined },
);
apiClients.set(serverKey, apiClient);

return apiClient;
};

Expand Down
6 changes: 0 additions & 6 deletions src/api/services/provideServices.ts

This file was deleted.

72 changes: 16 additions & 56 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,32 @@
import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit';
import type { Settings } from '@shlinkio/shlink-web-component/settings';
import { clsx } from 'clsx';
import type { FC } from 'react';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { Route, Routes, useLocation } from 'react-router';
import { AppUpdateBanner } from '../common/AppUpdateBanner';
import { Home } from '../common/Home';
import { MainHeader } from '../common/MainHeader';
import { NotFound } from '../common/NotFound';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServersMap } from '../servers/data';
import { ShlinkVersionsContainer } from '../common/ShlinkVersionsContainer';
import { ShlinkWebComponentContainer } from '../common/ShlinkWebComponentContainer';
import { CreateServer } from '../servers/CreateServer';
import { EditServer } from '../servers/EditServer';
import { ManageServers } from '../servers/ManageServers';
import { useLoadRemoteServers } from '../servers/reducers/remoteServers';
import { useSettings } from '../settings/reducers/settings';
import { Settings } from '../settings/Settings';
import { forceUpdate } from '../utils/helpers/sw';
import { useAppUpdated } from './reducers/appUpdates';

type AppProps = {
fetchServers: () => void;
servers: ServersMap;
settings: Settings;
resetAppUpdate: () => void;
appUpdated: boolean;
};

type AppDeps = {
MainHeader: FC;
Home: FC;
ShlinkWebComponentContainer: FC;
CreateServer: FC;
EditServer: FC;
Settings: FC;
ManageServers: FC;
ShlinkVersionsContainer: FC;
};
export const App: FC = () => {
const { appUpdated, resetAppUpdate } = useAppUpdated();

const App: FCWithDeps<AppProps, AppDeps> = (
{ fetchServers, servers, settings, appUpdated, resetAppUpdate },
) => {
const {
MainHeader,
Home,
ShlinkWebComponentContainer,
CreateServer,
EditServer,
Settings,
ManageServers,
ShlinkVersionsContainer,
} = useDependencies(App);
useLoadRemoteServers();

const location = useLocation();
const initialServers = useRef(servers);
const isHome = location.pathname === '/';

useEffect(() => {
// Try to fetch the remote servers if the list is empty during first render.
// We use a ref because we don't care if the servers list becomes empty later.
if (Object.keys(initialServers.current).length === 0) {
fetchServers();
}
}, [fetchServers]);

const { settings } = useSettings();
useEffect(() => {
changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme());
}, [settings.ui?.theme]);
Expand Down Expand Up @@ -98,14 +69,3 @@ const App: FCWithDeps<AppProps, AppDeps> = (
</div>
);
};

export const AppFactory = componentFactory(App, [
'MainHeader',
'Home',
'ShlinkWebComponentContainer',
'CreateServer',
'EditServer',
'Settings',
'ManageServers',
'ShlinkVersionsContainer',
]);
11 changes: 11 additions & 0 deletions src/app/reducers/appUpdates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '../../store';

const { actions, reducer } = createSlice({
name: 'shlink/appUpdates',
Expand All @@ -12,3 +14,12 @@ const { actions, reducer } = createSlice({
export const { appUpdateAvailable, resetAppUpdate } = actions;

export const appUpdatesReducer = reducer;

export const useAppUpdated = () => {
const dispatch = useAppDispatch();
const appUpdateAvailable = useCallback(() => dispatch(actions.appUpdateAvailable()), [dispatch]);
const resetAppUpdate = useCallback(() => dispatch(actions.resetAppUpdate()), [dispatch]);
const appUpdated = useAppSelector((state) => state.appUpdated);

return { appUpdated, appUpdateAvailable, resetAppUpdate };
};
14 changes: 0 additions & 14 deletions src/app/services/provideServices.ts

This file was deleted.

13 changes: 6 additions & 7 deletions src/common/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Card } from '@shlinkio/shlink-frontend-kit';
import { clsx } from 'clsx';
import type { FC } from 'react';
import { useEffect } from 'react';
import { ExternalLink } from 'react-external-link';
import { useNavigate } from 'react-router';
import type { ServersMap } from '../servers/data';
import { withoutSelectedServer } from '../servers/helpers/withoutSelectedServer';
import { useServers } from '../servers/reducers/servers';
import { ServersListGroup } from '../servers/ServersListGroup';
import { ShlinkLogo } from './img/ShlinkLogo';

export type HomeProps = {
servers: ServersMap;
};

export const Home = ({ servers }: HomeProps) => {
export const Home: FC = withoutSelectedServer(() => {
const navigate = useNavigate();
const { servers } = useServers();
const serversList = Object.values(servers);
const hasServers = serversList.length > 0;

Expand Down Expand Up @@ -68,4 +67,4 @@ export const Home = ({ servers }: HomeProps) => {
</Card>
</div>
);
};
});
12 changes: 2 additions & 10 deletions src/common/MainHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavBar } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link, useLocation } from 'react-router';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import { ServersDropdown } from '../servers/ServersDropdown';
import { ShlinkLogo } from './img/ShlinkLogo';

type MainHeaderDeps = {
ServersDropdown: FC;
};

const MainHeader: FCWithDeps<unknown, MainHeaderDeps> = () => {
const { ServersDropdown } = useDependencies(MainHeader);
export const MainHeader: FC = () => {
const { pathname } = useLocation();

const settingsPath = '/settings';
Expand All @@ -37,5 +31,3 @@ const MainHeader: FCWithDeps<unknown, MainHeaderDeps> = () => {
</NavBar>
);
};

export const MainHeaderFactory = componentFactory(MainHeader, ['ServersDropdown']);
21 changes: 10 additions & 11 deletions src/common/ShlinkVersionsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { clsx } from 'clsx';
import type { SelectedServer } from '../servers/data';
import { isReachableServer } from '../servers/data';
import { useSelectedServer } from '../servers/reducers/selectedServer';
import { ShlinkVersions } from './ShlinkVersions';

export type ShlinkVersionsContainerProps = {
selectedServer: SelectedServer;
export const ShlinkVersionsContainer = () => {
const { selectedServer } = useSelectedServer();
return (
<div
className={clsx('text-center', { 'md:ml-(--aside-menu-width)': isReachableServer(selectedServer) })}
>
<ShlinkVersions selectedServer={selectedServer} />
</div>
);
};

export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => (
<div
className={clsx('text-center', { 'md:ml-(--aside-menu-width)': isReachableServer(selectedServer) })}
>
<ShlinkVersions selectedServer={selectedServer} />
</div>
);
39 changes: 16 additions & 23 deletions src/common/ShlinkWebComponentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,34 @@ import {
ShlinkSidebarVisibilityProvider,
ShlinkWebComponent,
} from '@shlinkio/shlink-web-component';
import type { Settings } from '@shlinkio/shlink-web-component/settings';
import type { FC } from 'react';
import { memo } from 'react';
import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import { withDependencies } from '../container/context';
import { isReachableServer } from '../servers/data';
import type { WithSelectedServerProps } from '../servers/helpers/withSelectedServer';
import { ServerError } from '../servers/helpers/ServerError';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSelectedServer } from '../servers/reducers/selectedServer';
import { useSettings } from '../settings/reducers/settings';
import { NotFound } from './NotFound';

type ShlinkWebComponentContainerProps = WithSelectedServerProps & {
settings: Settings;
export type ShlinkWebComponentContainerProps = {
TagColorsStorage: TagColorsStorage;
buildShlinkApiClient: ShlinkApiClientBuilder;
};

type ShlinkWebComponentContainerDeps = {
buildShlinkApiClient: ShlinkApiClientBuilder,
TagColorsStorage: TagColorsStorage,
ServerError: FC,
};

const ShlinkWebComponentContainer: FCWithDeps<
ShlinkWebComponentContainerProps,
ShlinkWebComponentContainerDeps
const ShlinkWebComponentContainerBase: FC<
ShlinkWebComponentContainerProps
// FIXME Using `memo` here to solve a flickering effect in charts.
// memo is probably not the right solution. The root cause is the withSelectedServer HOC, but I couldn't fix the
// extra rendering there.
// This should be revisited at some point.
> = withSelectedServer(memo(({ selectedServer, settings }) => {
const {
buildShlinkApiClient,
TagColorsStorage: tagColorsStorage,
ServerError,
} = useDependencies(ShlinkWebComponentContainer);
> = withSelectedServer(memo(({
buildShlinkApiClient,
TagColorsStorage: tagColorsStorage,
}) => {
const { selectedServer } = useSelectedServer();
const { settings } = useSettings();

if (!isReachableServer(selectedServer)) {
return <ServerError />;
Expand All @@ -62,8 +56,7 @@ const ShlinkWebComponentContainer: FCWithDeps<
);
}));

export const ShlinkWebComponentContainerFactory = componentFactory(ShlinkWebComponentContainer, [
export const ShlinkWebComponentContainer = withDependencies(ShlinkWebComponentContainerBase, [
'buildShlinkApiClient',
'TagColorsStorage',
'ServerError',
]);
Loading
Loading