refactor: render cached data directly if available during ssr

This commit is contained in:
Nicolas Meienberger 2026-02-28 00:59:05 +01:00
parent bba61c2a37
commit 66076e5841
6 changed files with 84 additions and 29 deletions

View file

@ -51,6 +51,7 @@ type Props = {
repos: Repository[];
scheduleNotifs: ScheduleNotification[];
mirrors: ScheduleMirror[];
snapshots?: Snapshot[];
};
scheduleId: string;
initialSnapshotId?: string;
@ -78,6 +79,7 @@ export function ScheduleDetailsPage(props: Props) {
failureReason,
} = useQuery({
...listSnapshotsOptions({ path: { shortId: schedule.repository.shortId }, query: { backupId: schedule.shortId } }),
initialData: loaderData.snapshots,
});
const updateSchedule = useMutation({

View file

@ -5,8 +5,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui
import { RepositoryInfoTabContent } from "../tabs/info";
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
import { useNavigate, useSearch } from "@tanstack/react-router";
import type { BackupSchedule, Snapshot } from "~/client/lib/types";
export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) {
export default function RepositoryDetailsPage({
repositoryId,
initialSnapshots,
initialBackupSchedules,
}: {
repositoryId: string;
initialSnapshots?: Snapshot[];
initialBackupSchedules?: BackupSchedule[];
}) {
const navigate = useNavigate();
const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId/" });
const activeTab = tab || "info";
@ -27,7 +36,11 @@ export default function RepositoryDetailsPage({ repositoryId }: { repositoryId:
</TabsContent>
<TabsContent value="snapshots">
<Suspense>
<RepositorySnapshotsTabContent repository={data} />
<RepositorySnapshotsTabContent
repository={data}
initialSnapshots={initialSnapshots}
initialBackupSchedules={initialBackupSchedules}
/>
</Suspense>
</TabsContent>
</Tabs>

View file

@ -11,23 +11,26 @@ import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Input } from "~/client/components/ui/input";
import { Table, TableBody, TableCell, TableRow } from "~/client/components/ui/table";
import type { Repository, Snapshot } from "~/client/lib/types";
import type { BackupSchedule, Repository, Snapshot } from "~/client/lib/types";
import { toast } from "sonner";
type Props = {
repository: Repository;
initialSnapshots?: Snapshot[];
initialBackupSchedules?: BackupSchedule[];
};
export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
export const RepositorySnapshotsTabContent = ({ repository, initialSnapshots, initialBackupSchedules }: Props) => {
const [searchQuery, setSearchQuery] = useState("");
const { data, isFetching, failureReason } = useQuery({
const { data, isPending, failureReason } = useQuery({
...listSnapshotsOptions({ path: { shortId: repository.shortId } }),
initialData: [],
initialData: initialSnapshots,
});
const schedules = useQuery({
...listBackupSchedulesOptions(),
initialData: initialBackupSchedules,
});
const refreshMutation = useMutation({
@ -44,7 +47,9 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
refreshMutation.mutate({ path: { shortId: repository.shortId } });
};
const filteredSnapshots = data.filter((snapshot: Snapshot) => {
const snapshots = data ?? [];
const filteredSnapshots = snapshots.filter((snapshot: Snapshot) => {
if (!searchQuery) return true;
const searchLower = searchQuery.toLowerCase();
@ -58,7 +63,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
);
});
const hasNoFilteredSnapshots = !filteredSnapshots?.length;
const hasNoFilteredSnapshots = !filteredSnapshots.length;
if (repository.status === "error") {
return (
@ -91,7 +96,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
);
}
if (isFetching && !data.length) {
if (isPending) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
@ -101,7 +106,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
);
}
if (!data.length) {
if (!snapshots.length) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center text-center py-16 px-4">
@ -131,7 +136,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
<div className="flex-1">
<CardTitle>Snapshots</CardTitle>
<CardDescription className="mt-1">
Backup snapshots stored in this repository. Total: {data.length}
Backup snapshots stored in this repository. Total: {snapshots.length}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
@ -180,7 +185,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
<span>
{hasNoFilteredSnapshots
? "No snapshots match filters."
: `Showing ${filteredSnapshots.length} of ${data.length}`}
: `Showing ${filteredSnapshots.length} of ${snapshots.length}`}
</span>
</div>
</Card>

View file

@ -9,6 +9,7 @@ import {
listSnapshotsOptions,
} from "~/client/api-client/@tanstack/react-query.gen";
import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-details";
import { prefetchOrSkip } from "~/utils/prefetch";
export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
component: RouteComponent,
@ -24,14 +25,21 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { shortId: backupId } }) }),
]);
void context.queryClient.prefetchQuery({
...listSnapshotsOptions({
path: { shortId: schedule.repository.shortId },
query: { backupId: schedule.shortId },
}),
const snapshotOptions = listSnapshotsOptions({
path: { shortId: schedule.repository.shortId },
query: { backupId: schedule.shortId },
});
return { schedule, notifs, repos, scheduleNotifs, mirrors };
await prefetchOrSkip(context.queryClient, snapshotOptions);
return {
schedule,
notifs,
repos,
scheduleNotifs,
mirrors,
snapshots: context.queryClient.getQueryData(snapshotOptions.queryKey),
};
},
staticData: {
breadcrumb: (match) => [

View file

@ -6,23 +6,26 @@ import {
listSnapshotsOptions,
} from "~/client/api-client/@tanstack/react-query.gen";
import RepositoryDetailsPage from "~/client/modules/repositories/routes/repository-details";
import { prefetchOrSkip } from "~/utils/prefetch";
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")({
component: RouteComponent,
errorComponent: (e) => <div>{e.error.message}</div>,
loader: async ({ params, context }) => {
void context.queryClient.prefetchQuery({
...listSnapshotsOptions({ path: { shortId: params.repositoryId } }),
});
void context.queryClient.prefetchQuery({
...listBackupSchedulesOptions(),
});
const snapshotOptions = listSnapshotsOptions({ path: { shortId: params.repositoryId } });
const schedulesOptions = listBackupSchedulesOptions();
const res = await context.queryClient.ensureQueryData({
...getRepositoryOptions({ path: { shortId: params.repositoryId } }),
});
const [res] = await Promise.all([
context.queryClient.ensureQueryData(getRepositoryOptions({ path: { shortId: params.repositoryId } })),
prefetchOrSkip(context.queryClient, snapshotOptions),
prefetchOrSkip(context.queryClient, schedulesOptions),
]);
return res;
return {
...res,
snapshots: context.queryClient.getQueryData(snapshotOptions.queryKey),
backupSchedules: context.queryClient.getQueryData(schedulesOptions.queryKey),
};
},
validateSearch: type({ tab: "string?" }),
staticData: {
@ -44,6 +47,13 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")
function RouteComponent() {
const { repositoryId } = Route.useParams();
const { snapshots, backupSchedules } = Route.useLoaderData();
return <RepositoryDetailsPage repositoryId={repositoryId} />;
return (
<RepositoryDetailsPage
repositoryId={repositoryId}
initialSnapshots={snapshots}
initialBackupSchedules={backupSchedules}
/>
);
}

17
app/utils/prefetch.ts Normal file
View file

@ -0,0 +1,17 @@
import type { FetchQueryOptions, QueryClient, QueryKey } from "@tanstack/react-query";
export async function prefetchOrSkip<
TQueryFnData = unknown,
TError = Error,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryClient: QueryClient,
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
timeoutMs = 150,
): Promise<void> {
await Promise.race([
queryClient.prefetchQuery(options),
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
]);
}