My App
Trpc

React Query & Tanstack Query

How to work with React Query & Tanstack Query

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
api/trpc/[trpc]/route.ts
import { createTRPCContext } from "@/trpc/init";
import { appRouter } from "@/trpc/routers/_app";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });
export { handler as GET, handler as POST };
trpc/init.ts
import { cache } from "react";
import { headers } from "next/headers";
import { initTRPC, TRPCError } from "@trpc/server";

const createTRPCContextInner = async () => {
  return {};
};

export const createTRPCContext = cache(createTRPCContextInner);
export const createTRPCContextForRoute = createTRPCContextInner;

// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.create({
  /**
   * @see https://trpc.io/docs/server/data-transformers
   */
  // transformer: superjson,
});
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;

// Infer the context type from createTRPCContextInner
type Context = Awaited<ReturnType<typeof createTRPCContextInner>>;

export const baseProcedure = t.procedure;
trpc/server.ts
import "server-only"; // <-- ensure this file cannot be imported from the client

import { cache } from "react";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";

import { makeQueryClient } from "./client";
import { createTRPCContext } from "./init";
import { appRouter } from "./routers/_app";

// IMPORTANT: Create a stable getter for the query client that
//            will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
  ctx: createTRPCContext,
  router: appRouter,
  queryClient: getQueryClient,
});

export const caller = appRouter.createCaller(createTRPCContext);
trpc/client.ts
"use client";

// ^-- to make sure we can mount the Provider from a server component
import { useEffect, useState } from "react";
import {
  defaultShouldDehydrateQuery,
  MutationCache,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCContext } from "@trpc/tanstack-react-query";

// import superjson from 'superjson';

import type { AppRouter } from "./routers/_app";

export function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 30 * 1000,
        retry: false,
      },
      dehydrate: {
        // serializeData: superjson.serialize,
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === "pending",
      },
      hydrate: {
        // deserializeData: superjson.deserialize,
      },
    },
    mutationCache: new MutationCache({
      onSuccess: (data: any) => {
        // If the data has an error, throw an error
        if (data.error) {
          throw new Error(data.error);
        }
      },
    }),
  });
}

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

let browserQueryClient: QueryClient;
export let queryClient: QueryClient;

function getQueryClient() {
  if (typeof window === "undefined") {
    // Server: always make a new query client
    return makeQueryClient();
  }
  // Browser: make a new query client if we don't already have one
  // This is very important, so we don't re-make a new client if React
  // suspends during the initial render. This may not be needed if we
  // have a suspense boundary BELOW the creation of the query client
  if (!browserQueryClient) {
    const client = makeQueryClient();
    browserQueryClient = client;
    queryClient = client;
  }
  return browserQueryClient;
}
function getUrl() {
  const base = (() => {
    if (typeof window !== "undefined") return "";
    if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
    return "http://localhost:3000";
  })();
  return `${base}/api/trpc`;
}
// Export raw tRPC client for direct client-side calls (not through React Query)
export let trpcClient:
  | ReturnType<typeof createTRPCClient<AppRouter>>
  | undefined;

export function TRPCReactProvider(
  props: Readonly<{
    children: React.ReactNode;
  }>
) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient();
  const [client] = useState(() => {
    const client = createTRPCClient<AppRouter>({
      links: [
        httpBatchLink({
          // transformer: superjson, <-- if you use a data transformer
          url: getUrl(),
        }),
      ],
    });
    // Store the raw client for direct calls
    if (typeof window !== "undefined") {
      trpcClient = client;
    }
    return client;
  });

  return (
    <QueryClientProvider client={queryClient}>
      <TRPCProvider trpcClient={client} queryClient={queryClient}>
        <StoredTRPC>
          {props.children}
          <ReactQueryDevtools />
        </StoredTRPC>
      </TRPCProvider>
    </QueryClientProvider>
  );
}

// Store the TRPC instance so it can be accessed without calling the hook
// This is useful for queryOptions() and mutationOptions() which aren't hooks
export let storedTRPC: ReturnType<typeof useTRPC> | undefined;

/**
 * Internal component that stores the TRPC instance
 * Automatically initializes storedTRPC when TRPCReactProvider mounts
 */
function StoredTRPC({ children }: { children: React.ReactNode }) {
  const trpc = useTRPC();

  useEffect(() => {
    if (typeof window !== "undefined") {
      storedTRPC = trpc;
    }
  }, [trpc]);

  return <>{children}</>;
}
trpc/routers/_app.ts
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server";

import { baseProcedure, createTRPCRouter } from "../init";

export const appRouter = createTRPCRouter({
  hello: baseProcedure.query(() => {
    return {
      message: "Hello, world!",
    };
  }),
});

// export type definition of API
export type AppRouter = typeof appRouter;
export type RouterOutputs = inferRouterOutputs<AppRouter>;
export type RouterInputs = inferRouterInputs<AppRouter>;