These hooks wrap the TypeScript client with React Query for caching, refetching, and pagination.
This is a suggested integrator pattern, not a mirror of the reference webapp. The reference FE is plain JavaScript with axios and Redux — it does not use React Query or generated clients. Adopt or ignore as suits your stack.

Setup

npm i @tanstack/react-query
App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const qc = new QueryClient({
  defaultOptions: { queries: { staleTime: 60_000, retry: 1 } },
});

export function App() {
  return (
    <QueryClientProvider client={qc}>
      <Routes />
    </QueryClientProvider>
  );
}

Generic hooks

These generic wrappers use any internally — they give you React Query ergonomics but not end-to-end path/param type safety. For strict typing, call fd.GET("/events/{id}", ...) directly (the TypeScript client is fully typed) and wrap only where you want caching. Don’t mistake the generic hook for a typed client.
src/fd/hooks.ts
import { useQuery, useMutation, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
import { fd } from "./client";

// Stable query key: include partnerId so cache is partitioned per tenant.
function keyFor(partnerId: string, path: string, params?: unknown) {
  return [partnerId, path, params ?? null] as const;
}

// Generic GET — NOT strictly typed (uses any). Convenience over safety.
export function useFd(
  partnerId: string, path: string, params?: any, opts?: Partial<UseQueryOptions<any>>,
) {
  return useQuery({
    queryKey: keyFor(partnerId, path, params),
    queryFn:  async () => {
      const { data, error } = await (fd as any).GET(path, { params });
      if (error) throw error;
      return data;
    },
    ...opts,
  });
}

// Generic mutation. `invalidate` takes the SAME [partnerId, path, params]
// key shape used by useFd — not concrete URL strings.
export function useFdMutation(
  method: "POST" | "PUT" | "DELETE" | "PATCH",
  path: string,
  opts: { invalidateKeys?: readonly unknown[][] } = {},
) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (vars: { params?: any; body?: any }) => {
      const fn = (fd as any)[method] as Function;
      const { data, error } = await fn(path, { params: vars.params, body: vars.body });
      if (error) throw error;
      return data;
    },
    onSuccess: () =>
      opts.invalidateKeys?.forEach((k) => qc.invalidateQueries({ queryKey: k })),
  });
}

Typed domain hooks (illustrative)

These bind partnerId and path params, and invalidate using the same key shape as the queries they affect.
src/fd/hooks/events.ts
import { useFd, useFdMutation } from "../hooks";

export const useEvent = (pid: string, id: string) =>
  useFd(pid, "/events/{id}", { path: { id } });

export const useEventsList = (pid: string, filters: any) =>
  useFd(pid, "/events/", { query: filters });

// Demand prediction: lookout_prediction_benchmark with required `type`.
export const useEventBenchmark = (pid: string, id: string, type: "capacity" | "ticket_count" = "capacity") =>
  useFd(pid, "/events/{id}/lookout_prediction_benchmark", { path: { id }, query: { type } });

// goal is enum-style: UTILIZATION | ROAS | VISIBILITY | LINK_CLICKS
export const useSuggestedTcs = (pid: string, id: string, budget: number, goal = "UTILIZATION") =>
  useFd(pid, "/events/{id}/suggested_tcs", { path: { id }, query: { budget, goal } });

// Mutation: pass the path param through mutate(...) — it IS required.
export const useSetFrontendStatus = (pid: string, eid: string) =>
  useFdMutation("PUT", "/events/{id}/setup_frontend_status", {
    invalidateKeys: [
      [pid, "/events/", undefined],
      [pid, "/events/{id}", { path: { id: eid } }],
    ],
  });
// usage:
//   setFlag.mutate({ params: { path: { id: eid } }, body: { frontend_status: "FLAGGED" } });
src/fd/hooks/wave.ts
export const useSetupProcess = (pid: string, eid: string) =>
  useFd(pid, "/setup_processes/{eid}", { path: { eid } });

export const useCreateSetup = (pid: string, eid: string) =>
  useFdMutation("POST", "/setup_processes/{eid}", {
    invalidateKeys: [[pid, "/setup_processes/{eid}", { path: { eid } }]],
  });

export const usePublishSetup = (pid: string, eid: string) =>
  useFdMutation("POST", "/setup_processes/{eid}/boost", {
    invalidateKeys: [
      [pid, "/setup_processes/{eid}", { path: { eid } }],
      [pid, "/events/{eid}/campaigns", { path: { eid } }],
    ],
  });

export const useEventCampaignResults = (pid: string, eid: string) =>
  useFd(pid, "/integrations/facebook/events/{eid}/campaign_results", { path: { eid } });

Usage

function EventDetail({ pid, eid }: { pid: string; eid: string }) {
  const event   = useEvent(pid, eid);
  const bench   = useEventBenchmark(pid, eid, "capacity");
  const tcs     = useSuggestedTcs(pid, eid, 5000);
  const setFlag = useSetFrontendStatus(pid, eid);

  if (event.isLoading) return <Skeleton />;
  if (event.error?.status === 404) return <NotFound />;

  return (
    <>
      <Hero event={event.data} />
      <DemandPrediction benchmark={bench.data} />
      <TcSuggestions tcs={tcs.data} />
      <button onClick={() =>
        setFlag.mutate({ params: { path: { id: eid } }, body: { frontend_status: "FLAGGED" } })
      }>
        Flag
      </button>
    </>
  );
}

Caveats

useFd uses any. For path/param/response type safety, call the typed fd.GET(...) directly.
/events/{id}/setup_frontend_status requires params.path.id. Pass it through mutate({ params: { path: { id } }, ... }) or bind it in the hook.
useFd keys on [partnerId, path, params]. Invalidate with that exact structure — not a concrete URL string like /events/${eid}, which won’t match.
Every key starts with partnerId so switching partners doesn’t show stale cross-tenant data.