In this comprehensive guide, we will explore the process of integrating tRPCReact Query v5, and Prisma within the Next.js 14 App RoutertRPC is a modern RPC framework that allows you to define your API using TypeScript interfaces and generates both server and client code for you.

With the recent changes in Next.js 14, where all components are treated as server components, setting up tRPC requires a series of steps. However, this article covers the entire journey, from initializing a new Next.js 14 project to configuring tRPC and React Query for data fetching and mutation, all the way to using Prisma for PostgreSQL database interaction.

Our goal is to build a simple full-stack application that showcases the integration of tRPC in the new Next.js 14 App Router. You will gain insights into data prefetching using hydration with tRPC and React Query. Without further ado, let’s dive into the details of this tutorial.

The tutorial’s source code is available on GitHub: https://github.com/wpcodevo/nextjs14-trpc-react-query

More practice:

Using tRPC with Next.js 14, React Query and Prisma

Bootstrapping the Next.js 14 Project

By the end of this tutorial, you will have a folder structure that resembles the screenshot below. You can compare your folder structure with the image to ensure that you are following the instructions correctly.

folder structure of the Next.js 14, React Query v5 and tRPC Project

The image below showcases the application we’re about to create, covering all the steps required to set up and use tRPC with Next.js 14. The app consists of two components: one serving as a form for data mutation through tRPC, and the other for querying users using tRPC and displaying the results.

App built with Next.js 14, React Query v5, Prisma, and tRPC

Let’s begin by creating a new Next.js 14 project. Open a terminal in the directory where you want to store the source code and execute the following command:


yarn create nextjs14-trpc-react-query
# or 
npx create-next-app@latest nextjs14-trpc-react-query
# or
pnpm create next-app nextjs14-trpc-react-query

You’ll be prompted to configure the project. Make sure to select ‘Yes‘ for TypeScript and ESLint. Choose ‘No‘ for the src/ directory and ‘Yes‘ for the app/ directory in the project structure. Accept the default import alias by pressing Enter.

Once all questions are answered, the Next.js 14 project will be generated, and the dependencies will be installed. Open the project in your preferred IDE or text editor after the installation is completed.

Next, navigate to the app/globals.css file and remove all CSS styles, leaving only the Tailwind CSS directives.


pnpm add @tanstack/react-query@4.18.0
pnpm add @tanstack/react-query-devtools@4.18.0
pnpm add -D @tanstack/eslint-plugin-query

pnpm add @trpc/server
pnpm add @trpc/client
pnpm add @trpc/react-query
pnpm add superjson
pnpm add zod

pnpm add @prisma/client
pnpm add -D prisma
# or
yarn add @tanstack/react-query@4.18.0
yarn add @tanstack/react-query-devtools@4.18.0
yarn add -D @tanstack/eslint-plugin-query

yarn add @trpc/server
yarn add @trpc/client
yarn add @trpc/react-query
yarn add superjson
yarn add zod

yarn add @prisma/client
yarn add -D prisma
# or
npm i @tanstack/react-query@4.18.0
npm i @tanstack/react-query-devtools@4.18.0
npm i -D @tanstack/eslint-plugin-query

npm i @trpc/server
npm i @trpc/client
npm i @trpc/react-query
npm i superjson
npm i zod

npm i @prisma/client
npm i -D prisma

Note: It’s important to install @tanstack/react-query version 4.18.0 since the @trpc/react-query package depends on this specific version as of the time of writing this article.

  • @tanstack/react-query: This package provides hooks and utilities for managing and caching asynchronous data.
  • @tanstack/react-query-devtools: A tool for debugging and inspecting the state of React Query queries.
  • @tanstack/eslint-plugin-query: A plugin aiding in catching bugs and inconsistencies while working with React Query.
  • @trpc/server: Facilitates building the server side of a TypeScript RPC (tRPC) service.
  • @trpc/client: Generates TypeScript types and hooks for calling tRPC methods on the client.
  • @trpc/react-query: Integrates tRPC with React Query.
  • superjson: Serialization library that works with tRPC for handling complex data type serialization and deserialization.
  • zod: A TypeScript-first schema declaration and validation library.
  • @prisma/client: Prisma Client generated by Prisma, offering type-safe database access for TypeScript.
  • prisma: The Prisma CLI used for database migrations, seeding, and other database-related tasks during development.

Setting up PostgreSQL with Docker

With all the dependencies installed, let’s proceed to set up a PostgreSQL server using Docker. To do this, create a docker-compose.yml file in the root level of your project and add the following Docker Compose configurations.

docker-compose.yml


version: '3'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - '6500:5432'
    volumes:
      - progresDB:/var/lib/postgresql/data
    env_file:
      - ./.env
  pgAdmin:
    image: dpage/pgadmin4
    container_name: pgAdmin
    env_file:
      - ./.env
    ports:
      - '5050:80'
volumes:
  progresDB:

In the Docker Compose file, we use the env_file property to read the PostgreSQL and pgAdmin credentials from a .env file. To make these credentials available, create a .env file in the root directory and add the following environment variables.

.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password123
POSTGRES_DB=trpc_prisma

DATABASE_URL=postgresql://postgres:password123@localhost:6500/trpc_prisma?schema=public

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

To start the PostgreSQL and pgAdmin servers in their dedicated Docker containers, execute the following command.


docker-compose up -d

Setting up Prisma ORM

Now let’s set up Prisma to help us in querying and mutating data in the PostgreSQL database. Open your terminal and execute this command to initialize Prisma in the project.


pnpm prisma init --datasource-provider postgresql
# or
npx prisma init --datasource-provider postgresql

Moving on, let’s define our Prisma schema. For this simple application, we will define a User model with basic fields like name, email, and role. There won’t be a password field since we won’t be handling authentication. To define the Prisma model, open the prisma/schema.prisma file and replace its existing content with the following.

prisma/schema.prisma


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int   @id @default(autoincrement())
  name      String   
  email    String @unique
  role String @default("user")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Execute the following command to generate the Prisma migration file, create the Prisma client, and synchronize the migrations with the PostgreSQL schema.


pnpm prisma migrate dev --name init
# or
npx prisma migrate dev --name init

Now, we need to create a singleton instance of the Prisma Client that we can use throughout the application to communicate with the PostgreSQL database. To achieve this, create a prisma-client.ts file within the prisma directory and include the following code:

prisma/prisma-client.ts


import { PrismaClient } from '@prisma/client';

const prismaClientSingleton = () => {
  return new PrismaClient();
};

declare global {
  var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

const prisma = globalThis.prisma ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

Setting up tRPC Client and Server

At this stage, we’re prepared to configure the tRPC client and server within the Next.js 14 project. The initial setup is straightforward, and we’ll progressively enhance it as we introduce additional features.

Creating React Query Client

To create the React Query Client for managing queries and mutations in the application, follow these steps:

  1. In the root directory, create a folder named utils.
  2. Within the utils folder, create a file named query-client.ts.
  3. Add the code below to the query-client.ts file.

utils/query-client.ts


import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 5 * 1000 } },
});
export default queryClient;

This code creates a centralized instance of the React Query QueryClient with default options, including a staleTime of 5 seconds.

Initializing tRPC Server

Next in our setup, we’ll create an instance of the tRPC server. This instance plays a crucial role in defining the tRPC API and handling incoming requests. To get started, go ahead and create a file named trpc-server.ts within the utils directory. Populate this file with the following code:

utils/trpc-server.ts


import { initTRPC } from '@trpc/server';
import SuperJSON from 'superjson';

export const t = initTRPC.create({
  transformer: SuperJSON,
});

Creating the tRPC API

Now, let’s utilize the tRPC server instance to define an RPC called healthchecker that returns a simple JSON object with status and message fields. This will allow us to test if our setup is correct. To accomplish this, follow these steps:

  1. Create an api directory within the app directory.
  2. Inside the api directory, create another folder named trpc.
  3. Within the trpc directory, create a file named trpc-router.ts.
  4. Add the following code to the trpc-router.ts file:

app/api/trpc/trpc-router.ts


import { t } from "@/utils/trpc-server";

export const appRouter = t.router({
  healthchecker: t.procedure.query(({ ctx }) => {
    return {
      status: "success",
      message: "Welcome to trpc with Next.js 14 and React Query",
    };
  }),
});

export type AppRouter = typeof appRouter;

To proceed, let’s create the tRPC HTTP handlers. Navigate to the app/api/trpc/ directory. Inside this directory, create a new folder named [trpc]. Within the [trpc] directory, create a route.ts file, and include the following code.

app/api/trpc/[trpc]/route.ts


import {
  FetchCreateContextFnOptions,
  fetchRequestHandler,
} from '@trpc/server/adapters/fetch';
import { appRouter } from '../trpc-router';

const handler = (request: Request) => {
  console.log(`incoming request ${request.url}`);
  return fetchRequestHandler({
    endpoint: '/api/trpc',
    req: request,
    router: appRouter,
    createContext: function (
      opts: FetchCreateContextFnOptions
    ): object | Promise<object> {
      return {};
    },
  });
};

export { handler as GET, handler as POST };

Initializing the tRPC Client

Now, let’s proceed to initialize the tRPC client, allowing us to communicate with the tRPC backend. Create a trpc.ts file in the utils folder and add the following code:

utils/trpc.ts


import type { AppRouter } from '@/app/api/trpc/trpc-router';
import { createTRPCReact } from '@trpc/react-query';

export const trpc = createTRPCReact<AppRouter>();

Creating the tRPC Provider

With the tRPC client instance in place, let’s utilize it to create the tRPC provider. Later, we’ll wrap it around the root node of our application to ensure that all children nodes have access to both the tRPC client and a React Query client. Create a trpc-provider.tsx file within the utils directory and add the following code:

utils/trpc-provider.tsx


'use client';

import { QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, getFetch, loggerLink } from '@trpc/client';
import { useState } from 'react';
import superjson from 'superjson';
import { trpc } from './trpc';
import queryClient from './query-client';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const url = process.env.NEXT_PUBLIC_VERCEL_URL
    ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
    : 'http://localhost:3000/api/trpc/';

  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        loggerLink({
          enabled: () => true,
        }),
        httpBatchLink({
          url,
          fetch: async (input, init?) => {
            const fetch = getFetch();
            return fetch(input, {
              ...init,
              credentials: 'include',
            });
          },
        }),
      ],
      transformer: superjson,
    })
  );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
        <ReactQueryDevtools />
      </QueryClientProvider>
    </trpc.Provider>
  );
};

To make the tRPC client and React Query client accessible to all components in the component tree, open the app/layout.tsx file and wrap the tRPC provider we created earlier around the {children} node.

app/layout.tsx


import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { TrpcProvider } from '@/utils/trpc-provider';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <TrpcProvider>
          <div>{children}</div>
        </TrpcProvider>
      </body>
    </html>
  );
}

Testing the tRPC Server and Client

Finally, let’s test both the tRPC client and server. To do this, we will make an RPC call to the healthchecker procedure defined on the backend and display the result. Open the app/page.tsx file and replace its content with the following code:

app/page.tsx


'use client';

import { trpc } from '@/utils/trpc';

export default function Home() {
  let { data, isLoading, isFetching } = trpc.healthchecker.useQuery();
  if (isLoading || isFetching) return <p>Loading...</p>;

  return (
    <div className='text-xl font-bold'>
      <h1>Status: {data?.status}</h1>
      <h1>Message: {data?.message}</h1>
    </div>
  );
}

Start the Next.js development server if you haven’t already and visit the root URL in your browser. You’ll notice a brief loading state while the tRPC client invokes the healthchecker procedure. Once the call is successful, the status and message should be displayed on the screen.

testing the healthchecker route of the Next.js 14 trpc api from the frontend

Creating the tRPC Backend

Now that we’ve confirmed our tRPC server and client can communicate without errors, let’s enhance the functionality by creating two additional RPCs—one for adding new users to the database and another for retrieving users.

Creating Validation Schemas

To ensure the integrity of data stored in the database, we’ll use Zod to create validation schemas. These schemas guarantee that users provide accurate data with the correct data types in the JSON payload. Begin by creating a serverdirectory at the root level. Within the server directory, create a file named user-schema.ts and add the following code:

server/user-schema.ts


import { TypeOf, number, object, string } from 'zod';

export const createUserSchema = object({
  name: string({ required_error: 'Name is required' }),
  email: string({ required_error: 'Email is required' }).email('Invalid email'),
});

export const filterQuery = object({
  limit: number().default(1),
  page: number().default(10),
});

export type CreateUserInput = TypeOf<typeof createUserSchema>;
export type FilterQueryInput = TypeOf<typeof filterQuery>;

Creating tRPC Procedure Handlers

With the validation schemas prepared, let’s proceed to create the two RPC handlers responsible for managing requests and interacting with the database. Here’s a concise overview of each handler:

  1. createUserHandler: This RPC adds a new user to the database using Prisma and responds with the newly added user.
  2. getUsersHandler: When invoked, this RPC queries the database for a paginated list of users and returns them as a response.

server/user-controller.ts


import prisma from '@/prisma/prisma-client';
import { TRPCError } from '@trpc/server';
import { CreateUserInput, FilterQueryInput } from './user-schema';

export const createUserHandler = async ({
  input,
}: {
  input: CreateUserInput;
}) => {
  try {
    const user = await prisma.user.create({
      data: input,
    });

    return {
      status: 'success',
      data: {
        user,
      },
    };
  } catch (err: any) {
    throw new TRPCError({
      code: 'INTERNAL_SERVER_ERROR',
      message: err.message,
    });
  }
};

export const getUsersHandler = async ({
  filterQuery,
}: {
  filterQuery: FilterQueryInput;
}) => {
  try {
    const { limit, page } = filterQuery;
    const take = limit || 10;
    const skip = (page - 1) * limit;

    const users = await prisma.user.findMany({
      skip,
      take,
    });

    return {
      status: 'success',
      results: users.length,
      data: {
        users,
      },
    };
  } catch (err: any) {
    throw new TRPCError({
      code: 'INTERNAL_SERVER_ERROR',
      message: err.message,
    });
  }
};

Creating tRPC Router

To invoke the tRPC handlers we created above, we need to create a tRPC router and define RPC procedures to call them. To do this, create a user-route.ts file within the server directory, and include the code below:

server/user-route.ts


import { createUserSchema, filterQuery } from './user-schema';
import { createUserHandler, getUsersHandler } from './user-controller';
import { t } from '@/utils/trpc-server';

const userRouter = t.router({
  createUser: t.procedure
    .input(createUserSchema)
    .mutation(({ input }) => createUserHandler({ input })),
  getUsers: t.procedure
    .input(filterQuery)
    .query(({ input }) => getUsersHandler({ filterQuery: input })),
});

export default userRouter;

Merging tRPC Routers

To complete the tRPC backend setup, we need to integrate or register the userRouter with the main tRPC router. This ensures that the tRPC server correctly directs requests to the specified tRPC procedures when they are invoked. Open the trpc-router.ts file in the app/api/trpc/ directory and add the following code:

app/api/trpc/trpc-router.ts


import userRouter from '@/server/user-route';
import { t } from '@/utils/trpc-server';

const healthCheckerRouter = t.router({
  healthchecker: t.procedure.query(({ ctx }) => {
    return {
      status: 'success',
      message: 'Welcome to trpc with Next.js 14 and React Query',
    };
  }),
});

export const appRouter = t.mergeRouters(userRouter, healthCheckerRouter);

export type AppRouter = typeof appRouter;

Creating the tRPC Frontend

Now, let’s begin building the tRPC frontend. The frontend will comprise two components: one for invoking the RPC responsible for inserting new users into the database, and the other for invoking the RPC responsible for retrieving users.

Performing Query with tRPC

To interact with the getUsers procedure, we’ll create a client component named ListUsers. Inside this component, we invoke the getUsers RPC to fetch a paginated list of users from the tRPC server. Once the data is retrieved, the component displays them in a grid format.

To implement this, begin by creating a ‘components‘ directory at the root level. Within the ‘components‘ folder, create a list-users.tsx file and add the following code.

components/list-user.tsx


'use client';

import { trpc } from '@/utils/trpc';
import Image from 'next/image';

export default function ListUsers() {
  const { data } = trpc.getUsers.useQuery({ limit: 10, page: 1 });

  return (
    <>
      {data?.data.users.length === 0 ? (
        <p className='text-center'>No Users Found</p>
      ) : (
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr 1fr 1fr',
            gap: 20,
          }}
        >
          {data?.data.users?.map((user) => (
            <div
              key={user.id}
              className='flex flex-col justify-center items-center border-gray-200 border'
            >
              <Image
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                width={180}
                height={180}
                className='block'
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      )}
    </>
  );
}

In the above code, we fetch user images from an external source, https://robohash.org/. To ensure this works seamlessly without errors, it’s essential to add the necessary configurations in the next.config.js file. Open the next.config.js file and replace its existing content with the following code:

next.config.js


/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'robohash.org',
      },
    ],
  },
};

module.exports = nextConfig;

Performing Mutation with tRPC

Let’s create a form to trigger the createUser RPC, which is responsible for mutating the database by adding new users. Navigate to the ‘components‘ directory and create a file named user-form.tsx. Populate the file with the following code:

components/user-form.tsx


'use client';

import queryClient from '@/utils/query-client';
import { trpc } from '@/utils/trpc';
import React, { useState } from 'react';

const UserForm = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const { mutate } = trpc.createUser.useMutation({
    onSettled: () => {
      setName('');
      setEmail('');
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries({
        queryKey: [
          ['getUsers'],
          { input: { limit: 10, page: 1 }, type: 'query' },
        ],
      });
    },
  });

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutate({ name, email });
  };

  return (
    <div className='bg-gray-300 p-8 pt-4 rounded shadow-md max-w-2xl w-full'>
      <h2 className='text-2xl font-semibold mb-4'>Submit Form</h2>

      <form onSubmit={handleSubmit} className='space-y-4'>
        <div className='flex space-x-4 items-end'>
          <div className='w-1/2'>
            <label
              htmlFor='name'
              className='block text-sm font-medium text-gray-700'
            >
              Name
            </label>
            <input
              type='text'
              id='name'
              name='name'
              value={name}
              onChange={(e) => setName(e.target.value)}
              className='mt-1 p-2 w-full border rounded-md'
              required
            />
          </div>

          <div className='w-1/2'>
            <label
              htmlFor='email'
              className='block text-sm font-medium text-gray-700'
            >
              Email
            </label>
            <input
              type='email'
              id='email'
              name='email'
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className='mt-1 p-2 w-full border rounded-md'
              required
            />
          </div>
          <div className='w-1/4'>
            <button
              type='submit'
              className='w-full block align-bottom bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600'
            >
              Submit
            </button>
          </div>
        </div>
      </form>
    </div>
  );
};

export default UserForm;

When the form is submitted, we extract the name and email values, passing them to the mutate function. This function invokes the createUser RPC, inserting the provided data into the database. Upon successful mutation, the getUsers RPC is triggered to fetch the latest list of users. If you’re curious about the lengthy query key, I simply copied it from the React Query devtool for convenience.

Prefetching Data with tRPC and React Query

For SEO optimization, we’ll prefetch the list of users on the server and leverage React Query’s <Hydrate /> component to provide the data to the component responsible for displaying them. In essence, you’ll discover how to prefetch data using tRPC and React Query in Next.js 14.

Creating a React Query Hydration Component

As we’ll be prefetching data in a server component where the <Hydrate /> component can’t be used directly, we need to create a client component that renders <Hydrate /> the component. Then, we can use the client component in the server component. Create a hydrate-client.tsx file and include the following code:

utils/hydrate-client.tsx


'use client';

import { Hydrate as RQHydrate, HydrateProps } from '@tanstack/react-query';

function Hydrate(props: HydrateProps) {
  return <RQHydrate {...props} />;
}

export default Hydrate;

Creating a tRPC Server-Side Caller

To implement server-side data prefetching using tRPC and React Query in Next.js 14, we utilize the createServerSideHelpers function provided by the @trpc/react-query library. This function returns an object containing methods that allow direct invocation of tRPC procedures on the server, eliminating the need for an HTTP request. These server-side helpers enable us to prefetch queries on the server, add them to the cache, dehydrate the cache, and send the data to the client.

Consequently, these queries are readily available on the client without the need for initial fetching. To create the server-side helpers, navigate to the trpc-router.ts file in the app/api/trpc/ directory and replace its existing content with the code below. This modified code exports the server-side helpers as a function.

app/api/trpc/trpc-router.ts


import userRouter from '@/server/user-route';
import { t } from '@/utils/trpc-server';
import { createServerSideHelpers } from '@trpc/react-query/server';
import SuperJSON from 'superjson';

const healthCheckerRouter = t.router({
  healthchecker: t.procedure.query(({ ctx }) => {
    return {
      status: 'success',
      message: 'Welcome to trpc with Next.js 14 and React Query',
    };
  }),
});

export const appRouter = t.mergeRouters(userRouter, healthCheckerRouter);

export const createSSRHelper = () =>
  createServerSideHelpers({
    router: appRouter,
    transformer: SuperJSON,
    ctx: () => {},
  });

export type AppRouter = typeof appRouter;

Within the app/page.tsx file, leverage the server-side helpers to prefetch the user list by invoking the getUsers procedure. Then, utilize the <Hydrate /> client component to ensure proper hydration and dehydration, making the data accessible to the <ListUsers /> client component.

app/page.tsx


import UserForm from '@/components/user-form';
import ListUsers from '@/components/list-user';
import { dehydrate } from '@tanstack/react-query';
import Hydrate from '@/utils/hydrate-client';
import { createSSRHelper } from './api/trpc/trpc-router';

export default async function Home() {
  const helpers = createSSRHelper();
  await helpers.getUsers.prefetch({ limit: 10, page: 1 });

  return (
    <Hydrate state={dehydrate(helpers.queryClient)}>
      <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
        <div className='w-full flex justify-center mb-8'>
          <UserForm />
        </div>
        <ListUsers />
      </main>
    </Hydrate>
  );
}

Launch the Next.js development server and navigate to the root URL to interact with the application. Here, you can add new users and witness their immediate addition to the page.

App built with Next.js 14, React Query v5, Prisma, and tRPC

When inspecting the HTML document in the Network tab, you will notice that the list of users is already included, indicating that the prefetching code is working as expected.

the data was included in the html along with the submit form since the data was prefetched on the server

Conclusion

And we’re done! Throughout this article, you learned how to set up and use tRPC with React Query in Next.js 14. I hope you found this helpful and enjoyable. If you have any questions or feedback, feel free to leave them in the comment section below. Thanks for reading!