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 { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore'; import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore'; import { useSettingsStore } from '@/store/settingsStore';
import { useRouter } from 'next/navigation';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useAppRouter } from '@/hooks/useAppRouter';
import { eventDispatcher } from '@/utils/event'; import { eventDispatcher } from '@/utils/event';
import { navigateToReader, showReaderWindow } from '@/utils/nav'; import { navigateToReader, showReaderWindow } from '@/utils/nav';
@@ -25,7 +25,11 @@ interface UseOpenBookOptions {
*/ */
export const useOpenBook = ({ setLoading, handleBookDownload }: UseOpenBookOptions) => { export const useOpenBook = ({ setLoading, handleBookDownload }: UseOpenBookOptions) => {
const _ = useTranslation(); 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 { envConfig, appService } = useEnv();
const { settings } = useSettingsStore(); const { settings } = useSettingsStore();
const { updateBook } = useLibraryStore(); const { updateBook } = useLibraryStore();
+7 -3
View File
@@ -4,7 +4,7 @@ import clsx from 'clsx';
import * as React from 'react'; import * as React from 'react';
import { MdChevronRight } from 'react-icons/md'; import { MdChevronRight } from 'react-icons/md';
import { useState, useRef, useEffect, Suspense, useCallback } from 'react'; 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 { Book } from '@/types/book';
import { AppService, DeleteAction } from '@/types/system'; import { AppService, DeleteAction } from '@/types/system';
@@ -155,6 +155,10 @@ const LibraryPageWithSearchParams = () => {
const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchParams | null }) => { const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchParams | null }) => {
const router = useAppRouter(); 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 { envConfig, appService } = useEnv();
const { token, user } = useAuth(); const { token, user } = useAuth();
const { const {
@@ -568,10 +572,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const bookIds = pendingNavigationBookIds; const bookIds = pendingNavigationBookIds;
setPendingNavigationBookIds(null); setPendingNavigationBookIds(null);
if (bookIds.length > 0) { if (bookIds.length > 0) {
navigateToReader(router, bookIds); navigateToReader(readerRouter, bookIds);
} }
} }
}, [pendingNavigationBookIds, appService, router]); }, [pendingNavigationBookIds, appService, readerRouter]);
useEffect(() => { useEffect(() => {
if (isInitiating.current) return; if (isInitiating.current) return;