Skip to content

Commit 32772c6

Browse files
authored
[Ref/Feat] Clean up our routing and make it faster (#3540)
* Move to createHashRouter This brings Heroic in line with modern react-router usage and allows for some new features * Make Library an Index Route * Pass `store` to WebView via params * Lazily load all routes The code for this is a little verbose, but it enables Vite to split code into multiple files. Build output before (note the large file sizes): build/assets/index.8e7c2c97.js 2.58 KiB / gzip: 1.01 KiB build/assets/index.ae6fe98a.js 564.94 KiB / gzip: 181.67 KiB build/assets/App.63d9df39.js 1208.01 KiB / gzip: 361.96 KiB Build output after: build/assets/index.4b57bd40.js 10.33 KiB / gzip: 3.71 KiB build/assets/index.dda1a522.js 10.12 KiB / gzip: 3.92 KiB build/assets/index.22363332.js 7.89 KiB / gzip: 3.02 KiB build/assets/index.d02459a2.js 4.05 KiB / gzip: 1.62 KiB build/assets/index.c7dbdf3e.js 3.56 KiB / gzip: 1.25 KiB build/assets/index.51987511.js 2.63 KiB / gzip: 1.02 KiB build/assets/index.11c6e1ab.js 41.67 KiB / gzip: 13.69 KiB build/assets/index.f9487201.js 350.03 KiB / gzip: 109.55 KiB build/assets/App.f1d932b1.js 417.92 KiB / gzip: 125.16 KiB build/assets/index.886bf77e.js 385.92 KiB / gzip: 111.35 KiB build/assets/index.55a76c73.js 564.94 KiB / gzip: 181.67 KiB This means that on Heroic startup, Electron has to run "only" ~570KiB of JavaScript instead of ~1.2MiB. In my tests, this speeds up startup time by roughly 10% * Fixup exports
1 parent e374731 commit 32772c6

File tree

7 files changed

+117
-85
lines changed

7 files changed

+117
-85
lines changed

src/frontend/App.tsx

Lines changed: 95 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
import React, { useContext } from 'react'
22

33
import './App.css'
4-
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'
5-
import Login from './screens/Login'
6-
import WebView from './screens/WebView'
7-
import { GamePage } from './screens/Game'
8-
import Library from './screens/Library'
9-
import WineManager from './screens/WineManager'
4+
import {
5+
createHashRouter,
6+
Navigate,
7+
Outlet,
8+
RouterProvider
9+
} from 'react-router-dom'
1010
import Sidebar from './components/UI/Sidebar'
11-
import Settings from './screens/Settings'
12-
import Accessibility from './screens/Accessibility'
1311
import ContextProvider from './state/ContextProvider'
1412
import { ControllerHints, Help, OfflineMessage } from './components/UI'
15-
import DownloadManager from './screens/DownloadManager'
1613
import DialogHandler from './components/UI/DialogHandler'
1714
import SettingsModal from './screens/Settings/components/SettingsModal'
1815
import ExternalLinkDialog from './components/UI/ExternalLinkDialog'
1916
import WindowControls from './components/UI/WindowControls'
2017
import classNames from 'classnames'
2118

22-
function App() {
19+
function Root() {
2320
const {
2421
isSettingsModalOpen,
2522
isRTL,
@@ -44,57 +41,95 @@ function App() {
4441
// disable dragging for all elements by default
4542
onDragStart={(e) => e.preventDefault()}
4643
>
47-
<HashRouter>
48-
<OfflineMessage />
49-
<Sidebar />
50-
<main className="content">
51-
<DialogHandler />
52-
{isSettingsModalOpen.gameInfo && (
53-
<SettingsModal
54-
gameInfo={isSettingsModalOpen.gameInfo}
55-
type={isSettingsModalOpen.type}
56-
/>
57-
)}
58-
<ExternalLinkDialog />
59-
<Routes>
60-
<Route path="/" element={<Navigate replace to="/library" />} />
61-
<Route path="/library" element={<Library />} />
62-
<Route path="login" element={<Login />} />
63-
<Route path="epicstore" element={<WebView store="epic" />} />
64-
<Route path="gogstore" element={<WebView store="gog" />} />
65-
<Route path="amazonstore" element={<WebView store="amazon" />} />
66-
<Route path="wiki" element={<WebView />} />
67-
<Route path="/gamepage">
68-
<Route path=":runner">
69-
<Route path=":appName" element={<GamePage />} />
70-
</Route>
71-
</Route>
72-
<Route path="/store-page" element={<WebView />} />
73-
<Route path="/last-url" element={<WebView />} />
74-
<Route path="loginweb">
75-
<Route path=":runner" element={<WebView />} />
76-
</Route>
77-
<Route path="settings">
78-
<Route path=":runner">
79-
<Route path=":appName">
80-
<Route path=":type" element={<Settings />} />
81-
</Route>
82-
</Route>
83-
</Route>
84-
<Route path="/wine-manager" element={<WineManager />} />
85-
<Route path="/download-manager" element={<DownloadManager />} />
86-
<Route path="/accessibility" element={<Accessibility />} />
87-
</Routes>
88-
</main>
89-
<div className="controller">
90-
<ControllerHints />
91-
<div className="simple-keyboard"></div>
92-
</div>
93-
{showOverlayControls && <WindowControls />}
94-
{experimentalFeatures.enableHelp && <Help items={help.items} />}
95-
</HashRouter>
44+
<OfflineMessage />
45+
<Sidebar />
46+
<main className="content">
47+
<DialogHandler />
48+
{isSettingsModalOpen.gameInfo && (
49+
<SettingsModal
50+
gameInfo={isSettingsModalOpen.gameInfo}
51+
type={isSettingsModalOpen.type}
52+
/>
53+
)}
54+
<ExternalLinkDialog />
55+
<Outlet />
56+
</main>
57+
<div className="controller">
58+
<ControllerHints />
59+
<div className="simple-keyboard"></div>
60+
</div>
61+
{showOverlayControls && <WindowControls />}
62+
{experimentalFeatures.enableHelp && <Help items={help.items} />}
9663
</div>
9764
)
9865
}
9966

100-
export default App
67+
function makeLazyFunc(
68+
importedFile: Promise<Record<'default', React.ComponentType>>
69+
) {
70+
return async () => {
71+
const component = await importedFile
72+
return { Component: component.default }
73+
}
74+
}
75+
76+
const router = createHashRouter([
77+
{
78+
path: '/',
79+
element: <Root />,
80+
children: [
81+
{
82+
index: true,
83+
lazy: makeLazyFunc(import('./screens/Library'))
84+
},
85+
{
86+
path: 'login',
87+
lazy: makeLazyFunc(import('./screens/Login'))
88+
},
89+
{
90+
path: 'store/:store',
91+
lazy: makeLazyFunc(import('./screens/WebView'))
92+
},
93+
{
94+
path: 'wiki',
95+
lazy: makeLazyFunc(import('./screens/WebView'))
96+
},
97+
{
98+
path: 'gamepage/:runner/:appName',
99+
lazy: makeLazyFunc(import('./screens/Game/GamePage'))
100+
},
101+
{
102+
path: 'store-page',
103+
lazy: makeLazyFunc(import('./screens/WebView'))
104+
},
105+
{
106+
path: 'loginweb/:runner',
107+
lazy: makeLazyFunc(import('./screens/WebView'))
108+
},
109+
{
110+
path: 'settings/:runner/:appName/:type',
111+
lazy: makeLazyFunc(import('./screens/Settings'))
112+
},
113+
{
114+
path: 'wine-manager',
115+
lazy: makeLazyFunc(import('./screens/WineManager'))
116+
},
117+
{
118+
path: 'download-manager',
119+
lazy: makeLazyFunc(import('./screens/DownloadManager'))
120+
},
121+
{
122+
path: 'accessibility',
123+
lazy: makeLazyFunc(import('./screens/Accessibility'))
124+
},
125+
{
126+
path: '*',
127+
element: <Navigate replace to="/" />
128+
}
129+
]
130+
}
131+
])
132+
133+
export default function App() {
134+
return <RouterProvider router={router} />
135+
}

src/frontend/components/UI/Sidebar/components/SidebarLinks/index.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ export default function SidebarLinks() {
8383
}
8484

8585
// By default, open Epic Store
86-
let defaultStore = '/epicstore'
86+
let defaultStore = 'epic'
8787
if (!epic.username && !gog.username && amazon.user_id) {
8888
// If only logged in to Amazon Games, open Amazon Gaming
89-
defaultStore = '/amazonstore'
89+
defaultStore = 'amazon'
9090
} else if (!epic.username && gog.username) {
9191
// Otherwise, if not logged in to Epic Games, open GOG Store
92-
defaultStore = '/gogstore'
92+
defaultStore = 'gog'
9393
}
9494

9595
// if we have a stored last-url, default to the `/last-url` route
@@ -124,7 +124,7 @@ export default function SidebarLinks() {
124124
active: isActive || location.pathname.includes('gamepage')
125125
})
126126
}
127-
to={'/library'}
127+
to={'/'}
128128
onClick={async () => handleRefresh()}
129129
>
130130
<>
@@ -141,7 +141,7 @@ export default function SidebarLinks() {
141141
active: isActive || location.pathname.includes('store')
142142
})
143143
}
144-
to={defaultStore}
144+
to={`/store/${defaultStore}`}
145145
>
146146
<>
147147
<div className="Sidebar__itemIcon">
@@ -159,7 +159,7 @@ export default function SidebarLinks() {
159159
active: isActive
160160
})
161161
}
162-
to="/epicstore"
162+
to="/store/epic"
163163
>
164164
<span>{t('store', 'Epic Store')}</span>
165165
</NavLink>
@@ -170,7 +170,7 @@ export default function SidebarLinks() {
170170
active: isActive
171171
})
172172
}
173-
to="/gogstore"
173+
to="/store/gog"
174174
>
175175
<span>{t('gog-store', 'GOG Store')}</span>
176176
</NavLink>
@@ -181,7 +181,7 @@ export default function SidebarLinks() {
181181
active: isActive
182182
})
183183
}
184-
to="/amazonstore"
184+
to="/store/amazon"
185185
>
186186
<span>{t('prime-gaming', 'Prime Gaming')}</span>
187187
</NavLink>

src/frontend/screens/Game/GamePage/components/DotsMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useContext, useState } from 'react'
22
import GameContext from '../../GameContext'
33
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4-
import { GameSubMenu } from '../..'
4+
import GameSubMenu from '../../GameSubMenu'
55
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
66
import { GameInfo } from 'common/types'
77
import {

src/frontend/screens/Game/index.tsx

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/frontend/screens/Login/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default React.memo(function NewLogin() {
7272

7373
async function handleLibraryClick() {
7474
await refreshLibrary({ runInBackground: false })
75-
navigate('/library')
75+
navigate('/')
7676
}
7777

7878
return (

src/frontend/screens/Settings/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function Settings() {
8787
if (!fromGameCard) {
8888
returnPath = `/gamepage/${runner}/${appName}`
8989
if (returnPath.includes('default')) {
90-
returnPath = '/library'
90+
returnPath = '/'
9191
}
9292
}
9393

src/frontend/screens/WebView/index.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { useNavigate, useLocation, useParams } from 'react-router'
1111
import { ToggleSwitch, UpdateComponent } from 'frontend/components/UI'
1212
import WebviewControls from 'frontend/components/UI/WebviewControls'
1313
import ContextProvider from 'frontend/state/ContextProvider'
14-
import { Runner } from 'common/types'
1514
import './index.css'
1615
import LoginWarning from '../Login/components/LoginWarning'
1716
import { NileLoginData } from 'common/types/nile'
@@ -21,11 +20,7 @@ import {
2120
DialogHeader
2221
} from 'frontend/components/UI/Dialog'
2322

24-
interface Props {
25-
store?: 'epic' | 'gog' | 'amazon'
26-
}
27-
28-
const validStoredUrl = (url: string, store: 'epic' | 'gog' | 'amazon') => {
23+
const validStoredUrl = (url: string, store: string) => {
2924
switch (store) {
3025
case 'epic':
3126
return url.includes('epicgames.com')
@@ -38,7 +33,7 @@ const validStoredUrl = (url: string, store: 'epic' | 'gog' | 'amazon') => {
3833
}
3934
}
4035

41-
export default function WebView({ store }: Props) {
36+
export default function WebView() {
4237
const { i18n } = useTranslation()
4338
const { pathname, search } = useLocation()
4439
const { t } = useTranslation()
@@ -56,6 +51,11 @@ export default function WebView({ store }: Props) {
5651
const navigate = useNavigate()
5752
const webviewRef = useRef<Electron.WebviewTag>(null)
5853

54+
// `store` is set to epic/gog/amazon depending on which storefront we're
55+
// supposed to show, `runner` is set to a runner if we're supposed to show its
56+
// login prompt
57+
const { store, runner } = useParams()
58+
5959
let lang = i18n.language
6060
if (i18n.language === 'pt') {
6161
lang = 'pt-BR'
@@ -73,12 +73,11 @@ export default function WebView({ store }: Props) {
7373
'https://auth.gog.com/auth?client_id=46899977096215655&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=code&layout=galaxy'
7474

7575
const trueAsStr = 'true' as unknown as boolean | undefined
76-
const { runner } = useParams() as { runner: Runner }
7776

7877
const urls: { [pathname: string]: string } = {
79-
'/epicstore': epicStore,
80-
'/gogstore': gogStore,
81-
'/amazonstore': amazonStore,
78+
'/store/epic': epicStore,
79+
'/store/gog': gogStore,
80+
'/store/amazon': amazonStore,
8281
'/wiki': wikiURL,
8382
'/loginEpic': epicLoginUrl,
8483
'/loginGOG': gogLoginUrl,
@@ -89,7 +88,7 @@ export default function WebView({ store }: Props) {
8988
let startUrl = urls[pathname]
9089

9190
if (store) {
92-
sessionStorage.setItem('last-store', `/${store}store`)
91+
sessionStorage.setItem('last-store', store)
9392
const lastUrl = sessionStorage.getItem(`last-url-${store}`)
9493
if (lastUrl && validStoredUrl(lastUrl, store)) {
9594
startUrl = lastUrl

0 commit comments

Comments
 (0)