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) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-06 04:12:05 +09:00
committed by GitHub
parent 75f1fafe9f
commit 52be6fa066
2 changed files with 13 additions and 5 deletions
@@ -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();
+7 -3
View File
@@ -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;