Create Next.JS client for tRPC server
PUBLISHED ON: Sunday, Mar 19, 2023
In the previous 🔗 article, we looked into setting up a tRPC API server. Now, let's look into using the API on the client side.
#
Create tRPC hooks for client side usage
// src/utils/trpc.ts
import { httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { NextPageContext } from 'next';
import superjson from 'superjson';
import type { AppRouter } from '~/server/routers/_app';
/**
* Here we define base url based on the different front-end services we plan to use
*/
export function getBaseUrl() {
if (typeof window !== 'undefined') {
return '';
}
return `http://localhost:${process.env.PORT ?? 3000}`;
}
/**
* Extend `NextPageContext` with meta data that can be picked up by `responseMeta()` when server-side rendering
*/
export interface SSRContext extends NextPageContext {
status?: number;
}
/**
* A set of strongly-typed React hooks from our `AppRouter` type signature with `createReactQueryHooks`
*/
export const trpc = createTRPCNext<AppRouter, SSRContext>({
config({ ctx }) {
return {
transformer: superjson,
links: [
// adds pretty logs to the console in development and logs errors in production
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
httpBatchLink({
// If we want to use SSR, we need to use the server's full URL
url: `${getBaseUrl()}/api/trpc`,
// Set custom request headers on every request from tRPC
headers() {
if (ctx?.req) {
// To use SSR properly, we need to forward the client's headers to the server
// This is so that we can pass through things like cookies when we're server-side rendering
// When using Node 18, omit the "connection" header
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
connection: _connection,
...headers
} = ctx.req.headers;
return {
...headers,
// Optional: inform server that it's an SSR request
'x-ssr': '1',
};
}
return {};
},
}),
],
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 60,
},
},
},
};
},
// client is not going to see the requests in the networks tab
ssr: true,
/**
* Set headers or status code when doing SSR
*/
responseMeta(opts) {
const ctx = opts.ctx as SSRContext;
if (ctx.status) {
// If HTTP status set, propagate that
return {
status: ctx.status,
};
}
const error = opts.clientErrors[0];
if (error) {
// Propagate http first error from API calls
return {
status: error.data?.httpStatus ?? 500,
};
}
return {};
},
});
export type RouterInput = inferRouterInputs<AppRouter>;
export type RouterOutput = inferRouterOutputs<AppRouter>;
#
Setup tRPC HTTP response handler
// src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { createContext } from '~/server/context';
import { appRouter } from '~/server/routers/_app';
export default createNextApiHandler({
router: appRouter,
createContext,
onError({ error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
console.error('Something went wrong', error);
} else {
console.log({ error });
}
},
batching: {
enabled: true,
},
});
Now we are ready to use tRPC in React components.
#
Create React context for user
import { createContext, ReactNode, useContext } from 'react';
import { RouterOutput } from '~/utils/trpc';
const UserContext = createContext<RouterOutput['users']['me']>(null);
function UserContextProvider({
children,
value,
}: {
children: ReactNode;
value: RouterOutput['users']['me'] | undefined;
}) {
return (
<UserContext.Provider value={value || null}>
{children}
</UserContext.Provider>
);
}
const useUserContext = () => useContext(UserContext);
export { useUserContext, UserContextProvider };
#
Login flow
The login flow would consist of:
- Login user
- Request OTP
- Navigate to verify OTP screen
import { trpc } from '~/utils/trpc';
const requestOtp = trpc.users['request-otp'].useMutation({
onSuccess() {
router.push('/verify-otp');
},
});
const login = trpc.users['login'].useMutation({
onSuccess() {
// Request OTP
requestOtp.mutate({
email: methods.getValues('email'),
});
},
});
function onSubmit(values: LoginUserInput) {
login.mutate({
...values,
});
}
#
Registration flow
const requestOtp = trpc.users['request-otp'].useMutation({
onSuccess() {
router.push('/verify-otp');
},
});
// error.message can be used to display the message in case of error
const { mutate, error } = trpc.users['register-user'].useMutation({
onSuccess() {
requestOtp.mutate({
email: methods.getValues('email'),
});
},
});
/**
* export type CreateUserInput = TypeOf<typeof registerUserSchema>;
* For more info,
* @link https://manojp1991.dev/article/2023/create-a-trpc-api
*/
function onSubmit(values: CreateUserInput) {
mutate(values);
}
#
Verify OTP screen
const { data, mutate, error } = trpc.users['verify-otp'].useMutation({
onSuccess() {
router.replace(data?.redirect || '/');
},
});
function onSubmit(values: VerifyOtpInput) {
mutate({
...values,
});
}
#
Create a post
const { mutate, error } = trpc.posts['add'].useMutation({
onSuccess({ id }) {
router.push(`/posts/${id}`);
},
});
/**
* export type CreatePostInput = z.infer<typeof createPostSchema>;
*/
function onSubmit(values: CreatePostInput) {
mutate(values);
}
#
List posts screen
const postsQuery = trpc.posts['list'].useInfiniteQuery(
{
limit: 5,
},
{
getPreviousPageParam(lastPage) {
return lastPage.nextCursor;
},
},
);
#
Get post by id
const { data, error, isLoading } = trpc.posts.byId.useQuery({ id });
#
Reference