Use when refactoring a detail page to eliminate prop passthrough by giving each section its own query. Invoke with page path as argument...
Refactor detail pages so every section owns its own data. No prop passthrough from parent queries.
Core technique: TanStack React Query select option — one shared cache entry, many typed views.
/query-autonomy app/(protected)/media/movies/[id]
Argument: relative path to a page directory containing _components/.
Scan the page and present findings. Do NOT write code yet.
page.tsx — identify the root ID param (e.g., tmdbId, showId)_components/useQuery, owns its datatmdbId for its own query + movieTitle from parent), note the extra prop passthroughPresent a table to the user:
AUTONOMOUS (no changes needed):
CastSection → movieCreditsQueryOptions(tmdbId)
CrewSection → movieCreditsQueryOptions(tmdbId)
GallerySection → movieImagesQueryOptions(tmdbId) + receives movieTitle prop
KeywordsSection → movieKeywordsQueryOptions(tmdbId)
LibraryStatusBadge → movieLibraryInfoQueryOptions(tmdbId)
RECEIVERS (will get their own query):
OverviewSection ← parent: OverviewTab
base query: movieDetailQueryOptions(tmdbId)
fields used: overview
new option: movieOverviewQueryOptions (select: movie.overview)
DetailsSection ← parent: OverviewTab
base query: movieDetailQueryOptions(tmdbId)
fields used: genres
new option: movieGenresQueryOptions (select: movie.genres)
ProductionSection ← parent: OverviewTab
base query: movieDetailQueryOptions(tmdbId)
fields used: budget, revenue, productionCompanies
new option: movieProductionQueryOptions (select)
ExternalLinksSection ← parent: OverviewTab
base query: movieDetailQueryOptions(tmdbId)
fields used: imdbId, tmdbId, homepage
new option: movieExternalLinksQueryOptions (select)
PARENTS TO SIMPLIFY:
OverviewTab — currently fetches movieDetailQueryOptions, distributes to 4 children
after: becomes pure layout, passes only tmdbId to all children
EXTRA PROP PASSTHROUGH (autonomous components receiving additional props):
GallerySection — receives movieTitle from parent
option: add select-based query for the extra field, or merge into existing query
After presenting, ask: "Proceed with refactor?"
Add new options to the existing query options file (e.g., options/queries/movies/detail.ts).
Pattern — using select to narrow the base query:
// Existing base query (unchanged — shared cache entry)
export function movieDetailQueryOptions(tmdbId: number) {
return queryOptions({
queryKey: ['movies', 'detail', tmdbId] as const,
queryFn: () => fetchMovieDetail(tmdbId),
staleTime: 10 * 60 * 1000,
});
}
// NEW: Per-section views into the same cache
export function movieOverviewQueryOptions(tmdbId: number) {
return queryOptions({
...movieDetailQueryOptions(tmdbId),
select: (movie) => movie.overview,
});
}
export function movieProductionQueryOptions(tmdbId: number) {
return queryOptions({
...movieDetailQueryOptions(tmdbId),
select: (movie) => ({
budget: movie.budget,
revenue: movie.revenue,
productionCompanies: movie.productionCompanies,
}),
});
}
Key: Spread the base query options and add select. Same queryKey = same cache = one network request.
For each Receiver section:
'use client' directive (if not present){ movie: MovieBasic } → { tmdbId: number }useQuery with the new select-based query option<Query> component using project conventions:loading: — skeleton matching the section layouterror: — silent failure (() => null) for supplementary sectionssuccess: — existing render logic, now receiving narrowed data'use client';
import { useQuery } from '@tanstack/react-query';
import { movieOverviewQueryOptions } from '@/options/queries/movies/detail';
import { Query } from '@/components/query';
import { Skeleton } from '@/components/ui/skeleton';
function OverviewSectionLoading() {
return (
<section className="relative">
<div className="absolute -left-4 top-0 h-full w-1 rounded-full bg-gradient-to-b from-amber-500/20 via-amber-500/10 to-transparent" />
<div className="flex flex-col gap-2 pl-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</section>
);
}
interface OverviewSectionProps {
tmdbId: number;
}
function OverviewSection({ tmdbId }: OverviewSectionProps) {
const query = useQuery(movieOverviewQueryOptions(tmdbId));
return (
<Query
result={query}
callbacks={{
loading: OverviewSectionLoading,
error: () => null,
success: (overview) => (
<section className="relative">
<div className="absolute -left-4 top-0 h-full w-1 ..." />
<p className="...">{overview}</p>
</section>
),
}}
/>
);
}
Remove the parent's query and distribution logic. The parent becomes a pure layout:
// BEFORE: OverviewTab fetches and distributes
function OverviewTab({ tmdbId }: OverviewTabProps) {
const movieQuery = useQuery(movieDetailQueryOptions(tmdbId));
return (
<Query
result={movieQuery}
callbacks={{
success: (movie) => (
<>
<OverviewSection overview={movie.overview} />
<ProductionSection movie={movie} />
</>
),
}}
/>
);
}
// AFTER: Pure layout, all children autonomous
function OverviewTab({ tmdbId }: OverviewTabProps) {
return (
<div className="flex flex-col space-y-8">
<OverviewSection tmdbId={tmdbId} />
<GallerySection tmdbId={tmdbId} />
<CastSection tmdbId={tmdbId} />
<CrewSection tmdbId={tmdbId} />
<DetailsSection tmdbId={tmdbId} />
<ProductionSection tmdbId={tmdbId} />
<ExternalLinksSection tmdbId={tmdbId} />
</div>
);
}
Key changes to parent:
'use client' if no longer needed (parent has no hooks)useQuery import and callQuery/Queries wrapperFor components that are already Autonomous but receive additional props from a parent (e.g., GallerySection receiving movieTitle):
select-based query to fetch it independentlyQueries wrapper to combine its existing query with the new select-based queryRun bun check to verify all TypeScript types are correct after refactoring.
select over separate fetch functionsAll sections consuming MovieBasic slices hit the same TMDB endpoint (/movie/{id}). Creating separate fetch functions would mean:
select gives each section its own typed view into a single cache entry. One fetch, many consumers.