What to build
usePostLike and useSavedPost are 103-line, byte-identical hooks differing only in field names (user_has_liked/like_count vs user_has_saved/save_count), the API functions they call, their invalidation keys, and toast copy. The duplicated body holds subtle, fragile behavior — snapshot rollback, opposite-intent re-dispatch on success, pending coalescing — that today must be fixed in two places.
Collapse them into ONE deep module that takes a config object and hides the optimistic-update orchestration behind a small interface, instantiated once for likes and once for saves. The two paired vitest suites collapse into one parameterized suite that exercises both instances through the shared interface — the interface becomes the test surface.
Design constraint (load-bearing): the win only holds if the module is genuinely deep. A flat (fieldName, queryPath, mutations) signature would re-expose every varying part (two coupled fields — the boolean and the counter — a second invalidation key, and four toast strings), making the interface as wide as the body it hides → shallow again. Hide the orchestration behind a config object, not a parameter list.
Acceptance criteria
Blocked by
What to build
usePostLikeanduseSavedPostare 103-line, byte-identical hooks differing only in field names (user_has_liked/like_countvsuser_has_saved/save_count), the API functions they call, their invalidation keys, and toast copy. The duplicated body holds subtle, fragile behavior — snapshot rollback, opposite-intent re-dispatch on success, pending coalescing — that today must be fixed in two places.Collapse them into ONE deep module that takes a config object and hides the optimistic-update orchestration behind a small interface, instantiated once for likes and once for saves. The two paired vitest suites collapse into one parameterized suite that exercises both instances through the shared interface — the interface becomes the test surface.
Design constraint (load-bearing): the win only holds if the module is genuinely deep. A flat
(fieldName, queryPath, mutations)signature would re-expose every varying part (two coupled fields — the boolean and the counter — a second invalidation key, and four toast strings), making the interface as wide as the body it hides → shallow again. Hide the orchestration behind a config object, not a parameter list.Acceptance criteria
usePostLike/useSavedPostare thin instantiations of itImageDetailModal,ImageDetailContent); all tests passBlocked by
postKeysfactory key, not a locally-redefined literal)