From 52be6fa06643852e1739b1bcd47f1ea4209dce81 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 6 Jul 2026 04:12:05 +0900 Subject: [PATCH] fix(reader): open books without a View Transition to avoid timeout (#4949) useAppRouter wraps every navigation in a View Transition. Opening a book is a heavy render (the reader mounts and loads the book) that can overrun the transition's ~4s DOM-update budget and abort with a TimeoutError (Sentry READEST-9). Navigate to the reader with the plain router instead, matching every other into-reader path in the app; the transition router stays for lighter navigation. Applies to the tap-to-open flow (useOpenBook) and the post-import queued open (library/page). Fixes READEST-9 Co-authored-by: Claude Opus 4.8 (1M context) --- apps/readest-app/src/app/library/hooks/useOpenBook.ts | 8 ++++++-- apps/readest-app/src/app/library/page.tsx | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/readest-app/src/app/library/hooks/useOpenBook.ts b/apps/readest-app/src/app/library/hooks/useOpenBook.ts index 27a89593..f8ee700d 100644 --- a/apps/readest-app/src/app/library/hooks/useOpenBook.ts +++ b/apps/readest-app/src/app/library/hooks/useOpenBook.ts @@ -3,8 +3,8 @@ import { Book } from '@/types/book'; import { useEnv } from '@/context/EnvContext'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; +import { useRouter } from 'next/navigation'; import { useTranslation } from '@/hooks/useTranslation'; -import { useAppRouter } from '@/hooks/useAppRouter'; import { eventDispatcher } from '@/utils/event'; import { navigateToReader, showReaderWindow } from '@/utils/nav'; @@ -25,7 +25,11 @@ interface UseOpenBookOptions { */ export const useOpenBook = ({ setLoading, handleBookDownload }: UseOpenBookOptions) => { const _ = useTranslation(); - const router = useAppRouter(); + // Open the reader with the plain router, NOT the View Transition router: + // the reader's initial render is heavy enough to blow the transition's ~4s + // DOM-update budget, which aborts with a TimeoutError (Sentry READEST-9). + // This matches how every other into-reader path already navigates. + const router = useRouter(); const { envConfig, appService } = useEnv(); const { settings } = useSettingsStore(); const { updateBook } = useLibraryStore(); diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index a9224ca5..c7109af1 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import * as React from 'react'; import { MdChevronRight } from 'react-icons/md'; import { useState, useRef, useEffect, Suspense, useCallback } from 'react'; -import { ReadonlyURLSearchParams, useSearchParams } from 'next/navigation'; +import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation'; import { Book } from '@/types/book'; import { AppService, DeleteAction } from '@/types/system'; @@ -155,6 +155,10 @@ const LibraryPageWithSearchParams = () => { const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchParams | null }) => { const router = useAppRouter(); + // Opening the reader is a heavy render that overruns the View Transition + // DOM-update budget (TimeoutError, Sentry READEST-9), so navigate to it with + // the plain router; `router` above keeps transitions for lighter navigation. + const readerRouter = useRouter(); const { envConfig, appService } = useEnv(); const { token, user } = useAuth(); const { @@ -568,10 +572,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP const bookIds = pendingNavigationBookIds; setPendingNavigationBookIds(null); if (bookIds.length > 0) { - navigateToReader(router, bookIds); + navigateToReader(readerRouter, bookIds); } } - }, [pendingNavigationBookIds, appService, router]); + }, [pendingNavigationBookIds, appService, readerRouter]); useEffect(() => { if (isInitiating.current) return;