diff --git a/package.json b/package.json index 63407b8c..0a1a15e1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", "@fontsource/roboto": "5.0.13", + "@loadable/component": "^5.16.4", "@mui/icons-material": "5.15.15", "@mui/material": "5.15.15", "@mui/x-date-pickers": "7.3.1", @@ -52,6 +53,7 @@ "react-device-detect": "2.2.3", "react-dom": "18.3.0", "react-i18next": "14.1.1", + "react-lazily": "^0.9.2", "react-router-dom": "6.23.0", "react-virtuoso": "4.7.10", "stylis": "4.3.2", diff --git a/src/App.tsx b/src/App.tsx index 819d4b09..2f3b719b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,35 +11,39 @@ import CssBaseline from '@mui/material/CssBaseline'; import React, { useLayoutEffect } from 'react'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev'; +import { loadable } from 'react-lazily/loadable'; import { AppContext } from '@/components/context/AppContext'; -import { Browse } from '@/screens/Browse'; -import { DownloadQueue } from '@/screens/DownloadQueue'; -import { Library } from '@/screens/Library'; -import { Manga } from '@/screens/Manga'; -import { Reader } from '@/screens/Reader'; -import { SearchAll } from '@/screens/SearchAll'; -import { Settings } from '@/screens/Settings'; -import { About } from '@/screens/settings/About'; -import { Backup } from '@/screens/settings/Backup'; -import { Categories } from '@/screens/settings/Categories'; -import { DefaultReaderSettings } from '@/screens/settings/DefaultReaderSettings'; -import { SourceConfigure } from '@/screens/SourceConfigure'; -import { SourceMangas } from '@/screens/SourceMangas'; -import { Updates } from '@/screens/Updates'; import '@/i18n'; -import { LibrarySettings } from '@/screens/settings/LibrarySettings'; import { DefaultNavBar } from '@/components/navbar/DefaultNavBar'; -import { DownloadSettings } from '@/screens/settings/DownloadSettings.tsx'; -import { ServerSettings } from '@/screens/settings/ServerSettings.tsx'; -import { ServerUpdateChecker } from '@/components/util/ServerUpdateChecker.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { BrowseSettings } from '@/screens/settings/BrowseSettings.tsx'; -import { WebUISettings } from '@/screens/settings/WebUISettings.tsx'; -import { Migrate } from '@/screens/Migrate.tsx'; -import { DeviceSetting } from '@/components/settings/DeviceSetting.tsx'; -import { TrackingSettings } from '@/screens/settings/TrackingSettings.tsx'; -import { TrackerOAuthLogin } from '@/screens/TrackerOAuthLogin.tsx'; import { WebUIUpdateChecker } from '@/components/util/WebUIUpdateChecker.tsx'; +import { ServerUpdateChecker } from '@/components/util/ServerUpdateChecker.tsx'; +import { lazyLoadFallback } from '@/util/LazyLoad.tsx'; +import { ErrorBoundary } from '@/util/ErrorBoundary.tsx'; + +const { Browse } = loadable(() => import('@/screens/Browse'), lazyLoadFallback); +const { DownloadQueue } = loadable(() => import('@/screens/DownloadQueue'), lazyLoadFallback); +const { Library } = loadable(() => import('@/screens/Library'), lazyLoadFallback); +const { Manga } = loadable(() => import('@/screens/Manga'), lazyLoadFallback); +const { Reader } = loadable(() => import('@/screens/Reader'), lazyLoadFallback); +const { SearchAll } = loadable(() => import('@/screens/SearchAll'), lazyLoadFallback); +const { Settings } = loadable(() => import('@/screens/Settings'), lazyLoadFallback); +const { About } = loadable(() => import('@/screens/settings/About'), lazyLoadFallback); +const { Backup } = loadable(() => import('@/screens/settings/Backup'), lazyLoadFallback); +const { Categories } = loadable(() => import('@/screens/settings/Categories'), lazyLoadFallback); +const { DefaultReaderSettings } = loadable(() => import('@/screens/settings/DefaultReaderSettings'), lazyLoadFallback); +const { SourceConfigure } = loadable(() => import('@/screens/SourceConfigure'), lazyLoadFallback); +const { SourceMangas } = loadable(() => import('@/screens/SourceMangas'), lazyLoadFallback); +const { Updates } = loadable(() => import('@/screens/Updates'), lazyLoadFallback); +const { LibrarySettings } = loadable(() => import('@/screens/settings/LibrarySettings'), lazyLoadFallback); +const { DownloadSettings } = loadable(() => import('@/screens/settings/DownloadSettings.tsx'), lazyLoadFallback); +const { ServerSettings } = loadable(() => import('@/screens/settings/ServerSettings.tsx'), lazyLoadFallback); +const { BrowseSettings } = loadable(() => import('@/screens/settings/BrowseSettings.tsx'), lazyLoadFallback); +const { WebUISettings } = loadable(() => import('@/screens/settings/WebUISettings.tsx'), lazyLoadFallback); +const { Migrate } = loadable(() => import('@/screens/Migrate.tsx'), lazyLoadFallback); +const { DeviceSetting } = loadable(() => import('@/components/settings/DeviceSetting.tsx'), lazyLoadFallback); +const { TrackingSettings } = loadable(() => import('@/screens/settings/TrackingSettings.tsx'), lazyLoadFallback); +const { TrackerOAuthLogin } = loadable(() => import('@/screens/TrackerOAuthLogin.tsx'), lazyLoadFallback); if (process.env.NODE_ENV !== 'production') { // Adds messages only in a dev environment @@ -91,51 +95,55 @@ export const App: React.FC = () => ( overflow: 'auto', }} > - - {/* General Routes */} - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + + + {/* General Routes */} + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + - {/* Manga Routes */} + {/* Manga Routes */} - - } /> - } /> - } /> - } /> - - } /> - - - } /> - - } /> - } /> - } /> - - } /> - } /> - - } /> - + + } /> + } /> + } /> + } /> + + } /> + + + } /> + + } /> + } /> + } /> + + } /> + } /> + + } /> + + - - } /> - - + + + } /> + + + ); diff --git a/src/util/ErrorBoundary.tsx b/src/util/ErrorBoundary.tsx new file mode 100644 index 00000000..6ec2e845 --- /dev/null +++ b/src/util/ErrorBoundary.tsx @@ -0,0 +1,86 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { Component, ErrorInfo, ReactNode, useEffect, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +interface Props { + children?: ReactNode; + setTrackPathChange: (change: boolean) => void; +} + +interface State { + error: any; +} + +class RealErrorBoundary extends Component { + // eslint-disable-next-line react/state-in-constructor + public state: State = { error: null }; + + private prevPath: string = ''; + + public static getDerivedStateFromError(error: any): State { + // Update state so the next render will show the fallback UI. + return { error }; + } + + componentDidMount() { + this.prevPath = window.location.pathname; + } + + public componentDidUpdate() { + if (window.location.pathname !== this.prevPath) { + this.setState({ error: null }); + } + + this.prevPath = window.location.pathname; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + // eslint-disable-next-line react/destructuring-assignment + this.props.setTrackPathChange(true); + } + + public render() { + const { error } = this.state; + + if (error) { + return ( + <> +

Something went wrong.

+

{error.message ?? JSON.stringify(error)}

+ + ); + } + + const { children } = this.props; + return children; + } +} + +export const ErrorBoundary = ({ children }: { children: React.ReactNode }) => { + const [key, setKey] = useState(0); + const { pathname } = useLocation(); + const previousPathnameRef = useRef(pathname); + const [trackPathChange, setTrackPathChange] = useState(false); + + useEffect(() => { + if (trackPathChange && previousPathnameRef.current !== pathname) { + previousPathnameRef.current = pathname; + setKey((currentKey) => (currentKey + 1) % 999999); + setTrackPathChange(false); + } + }, [pathname, previousPathnameRef.current, trackPathChange]); + + return ( + + {children} + + ); +}; diff --git a/src/util/LazyLoad.tsx b/src/util/LazyLoad.tsx new file mode 100644 index 00000000..72fe1fd9 --- /dev/null +++ b/src/util/LazyLoad.tsx @@ -0,0 +1,11 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; + +export const lazyLoadFallback = { fallback: }; diff --git a/yarn.lock b/yarn.lock index 5f985838..266b584c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1123,7 +1123,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.18", "@babel/runtime@^7.8.4": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== @@ -2049,6 +2049,15 @@ resolved "https://registry.yarnpkg.com/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz#9d68877a489107411b953c54ea65d0658b515809" integrity sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew== +"@loadable/component@^5.16.4": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.16.4.tgz#583cb65945a5061ca3c6b90eb34835c154283ff4" + integrity sha512-fJWxx9b5WHX90QKmizo9B+es2so8DnBthI1mbflwCoOyvzEwxiZ/SVDCTtXEnHG72/kGBdzr297SSIekYtzSOQ== + dependencies: + "@babel/runtime" "^7.12.18" + hoist-non-react-statics "^3.3.1" + react-is "^16.12.0" + "@mui/base@5.0.0-beta.40", "@mui/base@^5.0.0-beta.40": version "5.0.0-beta.40" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40.tgz#1f8a782f1fbf3f84a961e954c8176b187de3dae2" @@ -5894,7 +5903,7 @@ react-i18next@14.1.1: "@babel/runtime" "^7.23.9" html-parse-stringify "^3.0.1" -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -5909,6 +5918,11 @@ react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-lazily@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/react-lazily/-/react-lazily-0.9.2.tgz#74596dbde43c8e0f607445da5c4839cf6cc48ab5" + integrity sha512-oBVRDQ+SuMPWenBO/0Kq+iZk34lOYJEmjiTto4bYRufndf8pux3E50BT3mJZbsq0vBsAVbX3fpQjlUvsXgDVag== + react-property@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6"