saved 17KB on frontend bundle size by switching to wouter
Massimo Melina committed
Jan 5, 2026 at 21:42 UTC
ba2618c9f9e1d66603737565c6b1880529e0441b
9 files changed
+60
-15
frontend/package.json
+2
-2
@@ -12,18 +12,18 @@
12
"@hfs/shared": "*",
13
"lodash": "^4.17.21",
14
"preact": "^10.28.1",
15
- "react-router-dom": "^6.1.1",
15
"tssrp6a": "^3.0.0",
16
"usehooks-ts": "^2.6.0",
17
"valtio": "^1.13.0",
18
+ "wouter": "^3.4.1",
19
"xxhashjs": "^0.2.2"
20
},
21
"devDependencies": {
22
+ "@preact/preset-vite": "^2.10.2",
23
"@types/lodash": "^4.14.178",
24
"@types/react": "^18.3.14",
25
"@types/react-dom": "^18.3.2",
26
"@types/xxhashjs": "^0.2.4",
26
- "@preact/preset-vite": "^2.10.2",
27
"@vitejs/plugin-legacy": "^6.0.0",
28
"sass": "^1.54.5",
29
"sonda": "^0.10.1",
frontend/src/App.ts
+2
-2
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { BrowserRouter, Route, Routes, useNavigate } from "react-router-dom"
3
+import { BrowserRouter, Route, Routes, useNavigate } from './router'
4
import { createElement as h, Fragment } from 'react'
5
import { BrowseFiles } from "./BrowseFiles"
6
import { alertDialog, Dialogs } from './dialog'
@@ -41,7 +41,7 @@ function App() {
41
h(Toasts),
42
h(Dialogs, {},
43
h(Routes, {},
44
- h(Route, { path:'*', element: h(BrowseFiles) })
44
+ h(Route, { path: '*', element: h(BrowseFiles) })
45
),
46
),
47
),
frontend/src/Breadcrumbs.ts
+1
-2
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Link, LinkProps } from 'react-router-dom'
3
+import { Link, LinkProps } from './router'
4
import { createElement as h, Fragment, ReactElement } from 'react'
5
import { getPrefixUrl, hIcon } from './misc'
6
import { DirEntry, state, useSnapState } from './state'
@@ -69,4 +69,3 @@ function Breadcrumb({ path, label, current, ...rest }: { current?: boolean, path
69
}
70
}, label)
71
}
72
-
frontend/src/BrowseFiles.ts
+1
-2
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Link, useNavigate } from 'react-router-dom'
3
+import { Link, useNavigate } from './router'
4
import {
5
createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState, useId
6
} from 'react'
@@ -401,4 +401,3 @@ export const EntryDetails = memo(({ entry, midnight }: { entry: DirEntry, midnig
401
402
const EntrySize = memo(({ s }: { s: DirEntry['s'] }) =>
403
s === undefined ? null : h(Bytes, { className: 'entry-size', bytes: s }) )
404
-
frontend/src/clip.ts
+2
-2
@@ -2,7 +2,7 @@ import { createElement as h, Fragment } from 'react'
2
import { DirList, state, useSnapState } from './state'
3
import { Btn } from './components'
4
import { alertDialog, toast } from './dialog'
5
-import { useNavigate } from 'react-router-dom'
5
+import { useNavigate } from './router'
6
import { apiCall } from '@hfs/shared/api'
7
import { reloadList, usePath } from './useFetchList'
8
import _ from 'lodash'
@@ -73,4 +73,4 @@ export function moveFiles(uri_from: string[], uri_to: string) {
73
reloadList()
74
return true
75
}, e => void alertDialog(e))
76
-}
\ No newline at end of file
76
+}
frontend/src/fileMenu.ts
+2
-2
@@ -8,7 +8,7 @@ import _ from 'lodash'
8
import { getEntryIcon, MISSING_PERM } from './BrowseFiles'
9
import { DirEntry, state } from './state'
10
import { deleteFiles } from './menu'
11
-import { Link, LinkProps } from 'react-router-dom'
11
+import { Link, LinkProps } from './router'
12
import { fileShow, getShowComponent } from './show'
13
import { alertDialog, promptDialog, toast } from './dialog'
14
import { apiCall, useApi } from '@hfs/shared/api'
@@ -240,4 +240,4 @@ function renderUploaderFromDetails(details: any) {
240
if (!details) return
241
const { upload: u } = details
242
return u && `${u.username||''}${prefix('@', u.ip)}`
243
-}
\ No newline at end of file
243
+}
frontend/src/router.ts
new
+47
@@ -0,0 +1,47 @@
1
+import { createElement as h, Fragment } from 'react'
2
+import { Link as WouterLink, Route as WouterRoute, Switch, useLocation as useWouterLocation } from 'wouter'
3
+import type { AnchorHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react'
4
+
5
+// this module is designed to offer a minimal compatibility interface as that of react-router-dom
6
+
7
+export type LinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {
8
+ to: string
9
+ reloadDocument?: boolean
10
+}
11
+
12
+export const Routes = Switch as unknown as ComponentType<any>
13
+export const BrowserRouter = Fragment
14
+
15
+export function Link({ to, reloadDocument, ...rest }: LinkProps) {
16
+ if (reloadDocument)
17
+ return h('a', { href: to, ...rest })
18
+ return h(WouterLink as unknown as ComponentType<any>, { href: to, ...rest })
19
+}
20
+
21
+export function useNavigate() {
22
+ const [, setLocation] = useWouterLocation()
23
+ return (to: string, options?: { replace?: boolean }) => {
24
+ setLocation(to, options)
25
+ }
26
+}
27
+
28
+export function useLocation(): { pathname: string } {
29
+ const [pathname] = useWouterLocation()
30
+ return { pathname }
31
+}
32
+
33
+type RouteProps = {
34
+ path?: string
35
+ element?: ReactElement
36
+ children?: ReactNode
37
+ component?: ComponentType<any>
38
+}
39
+
40
+export function Route({ path, element, children, component, ...rest }: RouteProps) {
41
+ const resolvedPath = path === '*' ? '/:rest*' : path
42
+ if (element)
43
+ return h(WouterRoute, { path: resolvedPath, ...rest }, element)
44
+ if (component)
45
+ return h(WouterRoute, { path: resolvedPath, component, ...rest })
46
+ return h(WouterRoute, { path: resolvedPath, ...rest }, children)
47
+}
frontend/src/upload.ts
+2
-2
@@ -13,7 +13,7 @@ import { alertDialog, promptDialog } from './dialog'
13
import { reloadList } from './useFetchList'
14
import { apiCall } from '@hfs/shared/api'
15
import { state, useSnapState } from './state'
16
-import { Link } from 'react-router-dom'
16
+import { Link } from './router'
17
import { LinkClosingDialog } from './fileMenu'
18
import {
19
abortCurrentUpload, enqueueUpload, getFilePath, normalizeAccept, resetCounters, resetReloadOnClose,
@@ -298,4 +298,4 @@ export async function createFolder() {
298
299
export function inputComment(filename: string, value?: string) {
300
return promptDialog(t('enter_comment', { name: filename }, "Comment for {name}"), { value, type: 'textarea' })
301
-}
\ No newline at end of file
301
+}
frontend/src/useFetchList.ts
+1
-1
@@ -11,7 +11,7 @@ import {
11
hfsEvent, LIST, urlParams, xlate, objFromKeys, getHFS,
12
HTTP_MESSAGES, HTTP_METHOD_NOT_ALLOWED, HTTP_UNAUTHORIZED,
13
} from './misc'
14
-import { useLocation, useNavigate } from 'react-router-dom'
14
+import { useLocation, useNavigate } from './router'
15
import { closeLoginDialog } from './login'
16
import { fileShow, getShowComponent } from './show'
17
import i18n from './i18n'