* fix(library): refresh book cover after editing metadata Editing a book's cover in Book Details and saving showed the old cover until a full reload, in two render paths: - Library grid: handleUpdateMetadata mutated the book object in place, so the memoized <BookCover> compared fields off the same (mutated) reference and skipped re-rendering. Build a new book object via the new getBookWithUpdatedMetadata helper instead of mutating. - Book Details view: BookDetailView renders cover/title/author from the modal's `book` prop, which the parent never re-passed after save. BookDetailModal now tracks the saved book locally (displayBook) and renders the view from it. Adds a unit test for the immutable helper, a BookDetailModal regression test (edit cover -> save -> view reflects it), and a sample-alice.txt fixture for TXT import testing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(agent): add cover-refresh stale-render memory Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
3.0 KiB
name, description, metadata
| name | description | metadata | ||||||
|---|---|---|---|---|---|---|---|---|
| cover-stale-inplace-mutation-memo | Library cover (or any memoized child) not updating until refresh — in-place object mutation defeats React.memo |
|
Symptom: edit a book's cover in Book Details → Save → return to library, the cover shows the OLD image; a full page refresh fixes it. Title/author DO update.
Root cause: handleUpdateMetadata in src/app/library/page.tsx mutated the existing book object IN PLACE (book.metadata = …; book.coverImageUrl = …; book.updatedAt = …) then passed that same reference to updateBook. <BookCover> (src/components/BookCover.tsx) is React.memo'd with a custom comparator reading coverImageUrl/metadata.coverImageUrl/updatedAt off the book. Because the previous-render snapshot (prevProps.book) is the same mutated object, every field compares equal → memo skips → cover never re-renders. Title updates because BookItem is NOT memoized. Refresh works because loadLibraryBooks (libraryService.ts) strips coverImageUrl on save and REGENERATES it from ${hash}/cover.png on load (the file was overwritten by updateCoverImage).
KEY INSIGHT: cloning inside updateBook would NOT fix it — once the original object is mutated, prevProps already reads the new values. The fix must leave the object React holds as prevProps untouched.
Fix (PR for fix/txt-open-with-conversion): pure helper getBookWithUpdatedMetadata(book, metadata) in src/utils/book.ts returns a NEW book object ({...book, metadata, title, author, primaryLanguage, updatedAt, coverImageUrl}); handleUpdateMetadata uses it instead of mutating. Cover URL is set from metadata.coverImageBlobUrl || metadata.coverImageUrl (cached/blob URL is a unique path = the new image; '_blank' for remove). Unit test asserts immutability of the input + new reference.
General rule: when a memoized child reads fields off a store object, NEVER mutate that object in place to "update" it — build a new object. Same trap could bite any React.memo field comparator in this codebase.
On-device CDP verification (reusable): the cover SET path uses the native Tauri file picker (selectFiles → openDialog), NOT automatable via CDP; the installed emulator app is the released bundled build (http://tauri.localhost/...), not the dev server. So I verified the MECHANISM directly in the live WebView: extracted the zustand library store from the React fiber tree (the library page calls useLibraryStore() with NO selector, so its fiber hook memoizedState holds the full state incl. library/setLibrary/updateBook), injected an Alice book, then A) mutated it in place + setLibrary([sameRef]) → rendered <img src> stayed stale (bug), B) setLibrary([{...book,coverImageUrl:NEW}]) → <img src> updated immediately (fix). Restore with Page.reload (injected book was in-memory only). See cdp-android-webview-profiling, android-cdp-e2e-lane.