← back to Open Seo
src/client/features/backlinks/useBacklinksDomainExpansion.ts
94 lines
import { useCallback, useEffect, useMemo, useState } from "react";
import { useQueries } from "@tanstack/react-query";
import { getBacklinksRows } from "@/serverFunctions/backlinks";
import type { BacklinksRow, BacklinksSearchState } from "./backlinksPageTypes";
const DOMAIN_LINKS_PAGE_SIZE = 100;
const DOMAIN_LINKS_STALE_TIME_MS = 5 * 60 * 1000;
export type BacklinksDomainEntry =
| { status: "loading" }
| { status: "error" }
| { status: "ready"; rows: BacklinksRow[] };
export type BacklinksDomainExpansion = {
expandedDomains: ReadonlySet<string>;
/** One entry per expanded domain; keyed by the row's raw domainFrom. */
entriesByDomain: Record<string, BacklinksDomainEntry>;
toggleDomain: (domain: string) => void;
};
/**
* Lazily loads the full link list for referring domains the user expands in
* the one-per-domain backlinks view. Each expansion is one billed DataForSEO
* request (capped at 100 links), cached client-side and in R2.
*/
export function useBacklinksDomainExpansion({
projectId,
searchState,
}: {
projectId: string;
searchState: BacklinksSearchState;
}): BacklinksDomainExpansion {
const { target, scope } = searchState;
const [expanded, setExpanded] = useState<string[]>([]);
// Collapse everything when the lookup changes.
useEffect(() => {
setExpanded([]);
}, [projectId, target, scope]);
const queries = useQueries({
queries: expanded.map((domain) => ({
queryKey: [
"backlinksDomainLinks",
projectId,
scope,
target,
domain,
] as const,
staleTime: DOMAIN_LINKS_STALE_TIME_MS,
queryFn: () =>
getBacklinksRows({
data: {
projectId,
target,
scope,
page: 1,
pageSize: DOMAIN_LINKS_PAGE_SIZE,
sortField: "rank",
sortOrder: "desc",
filters: { domainFrom: domain },
mode: "as_is",
},
}),
})),
});
const entriesByDomain = useMemo(() => {
const map: Record<string, BacklinksDomainEntry> = {};
expanded.forEach((domain, index) => {
const query = queries[index];
if (!query) return;
map[domain] = query.data
? { status: "ready", rows: query.data.rows }
: query.error
? { status: "error" }
: { status: "loading" };
});
return map;
}, [expanded, queries]);
const expandedDomains = useMemo(() => new Set(expanded), [expanded]);
const toggleDomain = useCallback((domain: string) => {
setExpanded((current) =>
current.includes(domain)
? current.filter((entry) => entry !== domain)
: [...current, domain],
);
}, []);
return { expandedDomains, entriesByDomain, toggleDomain };
}