Next.js integration for tRPC that enables end-to-end type-safe APIs with enhanced server-side rendering and static site generation capabilities.
—
Experimental server actions integration for form handling and mutations in Next.js App Router.
Create React hooks for handling tRPC server actions with loading states and error handling.
/**
* Creates a React hook factory for tRPC server actions
* @param opts - tRPC client configuration options
* @returns Function that creates action hooks for specific handlers
*/
function experimental_createActionHook<TInferrable extends InferrableClientTypes>(
opts: CreateTRPCClientOptions<TInferrable>
): <TDef extends ActionHandlerDef>(
handler: TRPCActionHandler<TDef>,
useActionOpts?: UseTRPCActionOptions<TDef>
) => UseTRPCActionResult<TDef>;
interface UseTRPCActionOptions<TDef extends ActionHandlerDef> {
/** Callback called on successful action completion */
onSuccess?: (result: TDef['output']) => MaybePromise<void> | void;
/** Callback called on action error */
onError?: (result: TRPCClientError<TDef['errorShape']>) => MaybePromise<void>;
}Usage Examples:
import { experimental_createActionHook, experimental_serverActionLink } from "@trpc/next/app-dir/client";
// Create the action hook factory
const useAction = experimental_createActionHook({
links: [experimental_serverActionLink()],
});
// Use in a component
function CreatePostForm() {
const createPost = useAction(createPostAction, {
onSuccess: (result) => {
console.log("Post created:", result.id);
},
onError: (error) => {
console.error("Failed to create post:", error.message);
},
});
const handleSubmit = (formData: FormData) => {
createPost.mutate(formData);
};
return (
<form action={handleSubmit}>
<input name="title" placeholder="Post title" />
<textarea name="content" placeholder="Post content" />
<button type="submit" disabled={createPost.status === "loading"}>
{createPost.status === "loading" ? "Creating..." : "Create Post"}
</button>
{createPost.error && <p>Error: {createPost.error.message}</p>}
</form>
);
}Create server action handlers that integrate with tRPC procedures.
/**
* Creates server action handlers that integrate with tRPC procedures
* @param t - tRPC instance with configuration
* @param opts - Context creation and error handling options
* @returns Function that creates action handlers for specific procedures
*/
function experimental_createServerActionHandler<TInstance extends { _config: RootConfig<AnyRootTypes> }>(
t: TInstance,
opts: CreateContextCallback<TInstance['_config']['$types']['ctx'], () => MaybePromise<TInstance['_config']['$types']['ctx']>> & {
/** Transform form data to a Record before passing it to the procedure (default: true) */
normalizeFormData?: boolean;
/** Called when an error occurs in the handler */
onError?: (opts: ErrorHandlerOptions<TInstance['_config']['$types']['ctx']>) => void;
/** Rethrow errors that should be handled by Next.js (default: true) */
rethrowNextErrors?: boolean;
}
): <TProc extends AnyProcedure>(proc: TProc) => TRPCActionHandler<inferActionDef<TInstance, TProc>>;
type TRPCActionHandler<TDef extends ActionHandlerDef> = (
input: FormData | TDef['input']
) => Promise<TRPCResponse<TDef['output'], TDef['errorShape']>>;Usage Examples:
import { experimental_createServerActionHandler } from "@trpc/next/app-dir/server";
import { z } from "zod";
// Initialize tRPC
const t = initTRPC.context<{ userId?: string }>().create();
const procedure = t.procedure;
// Create action handler factory
const createAction = experimental_createServerActionHandler(t, {
createContext: async () => {
// Get user from session, database, etc.
return { userId: "user123" };
},
onError: ({ error, ctx }) => {
console.error("Action error:", error, "Context:", ctx);
},
});
// Define a procedure
const createPostProcedure = procedure
.input(z.object({
title: z.string(),
content: z.string(),
}))
.mutation(async ({ input, ctx }) => {
// Your mutation logic here
return { id: "post123", ...input };
});
// Create the action handler
const createPostAction = createAction(createPostProcedure);
// Use in server component or route handler
export async function createPost(formData: FormData) {
"use server";
return createPostAction(formData);
}tRPC link that handles communication between client action hooks and server actions.
/**
* tRPC link that handles communication with server actions
* @param opts - Optional transformer configuration
* @returns tRPC link for server action communication
*/
function experimental_serverActionLink<TInferrable extends InferrableClientTypes>(
opts?: TransformerOptions<inferClientTypes<TInferrable>>
): TRPCLink<TInferrable>;Usage Examples:
import { experimental_serverActionLink } from "@trpc/next/app-dir/client";
import superjson from "superjson";
// Basic usage
const basicLink = experimental_serverActionLink();
// With transformer
const linkWithTransformer = experimental_serverActionLink({
transformer: superjson,
});
// Use in client configuration
const useAction = experimental_createActionHook({
links: [linkWithTransformer],
transformer: superjson,
});The action hook returns different states based on the current status of the action.
type UseTRPCActionResult<TDef extends ActionHandlerDef> =
| UseTRPCActionErrorResult<TDef>
| UseTRPCActionIdleResult<TDef>
| UseTRPCActionLoadingResult<TDef>
| UseTRPCActionSuccessResult<TDef>;
interface UseTRPCActionBaseResult<TDef extends ActionHandlerDef> {
mutate: (...args: MutationArgs<TDef>) => void;
mutateAsync: (...args: MutationArgs<TDef>) => Promise<TDef['output']>;
}
interface UseTRPCActionSuccessResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
data: TDef['output'];
error?: never;
status: 'success';
}
interface UseTRPCActionErrorResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
data?: never;
error: TRPCClientError<TDef['errorShape']>;
status: 'error';
}
interface UseTRPCActionIdleResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
data?: never;
error?: never;
status: 'idle';
}
interface UseTRPCActionLoadingResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
data?: never;
error?: never;
status: 'loading';
}Server actions can handle both FormData and typed objects.
// Server action that handles FormData
const createAction = experimental_createServerActionHandler(t, {
createContext: async () => ({}),
normalizeFormData: true, // Convert FormData to object
});
const signupProcedure = procedure
.input(z.object({
email: z.string().email(),
password: z.string().min(8),
terms: z.string().optional(),
}))
.mutation(async ({ input }) => {
// FormData is automatically converted to typed object
const user = await createUser({
email: input.email,
password: input.password,
acceptedTerms: input.terms === "on",
});
return user;
});
const signupAction = createAction(signupProcedure);
// Client component
function SignupForm() {
const signup = useAction(signupAction);
return (
<form action={signup.mutate}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<input name="terms" type="checkbox" />
<button type="submit">Sign Up</button>
</form>
);
}Comprehensive error handling with different error types.
const createAction = experimental_createServerActionHandler(t, {
createContext: async () => ({}),
onError: ({ error, ctx, input, path, type }) => {
// Log errors for monitoring
console.error("Server action error:", {
path,
type,
error: error.message,
code: error.code,
input,
});
// Send to error tracking service
if (error.code === "INTERNAL_SERVER_ERROR") {
sendToErrorTracking(error);
}
},
rethrowNextErrors: true, // Let Next.js handle redirect/notFound errors
});
// Client with error handling
function CreatePostForm() {
const createPost = useAction(createPostAction, {
onError: async (error) => {
if (error.data?.code === "UNAUTHORIZED") {
// Redirect to login
window.location.href = "/login";
} else {
// Show user-friendly error
toast.error("Failed to create post. Please try again.");
}
},
});
// Component implementation...
}Server actions work without JavaScript, providing progressive enhancement.
// Server component with progressive enhancement
function PostForm({ post }: { post?: Post }) {
return (
<form action={createOrUpdatePostAction}>
{post && <input type="hidden" name="id" value={post.id} />}
<input name="title" defaultValue={post?.title} required />
<textarea name="content" defaultValue={post?.content} required />
<button type="submit">
{post ? "Update" : "Create"} Post
</button>
</form>
);
}
// Enhanced client component
"use client";
function EnhancedPostForm({ post }: { post?: Post }) {
const savePost = useAction(createOrUpdatePostAction, {
onSuccess: () => {
toast.success("Post saved!");
router.refresh();
},
});
return (
<form action={savePost.mutate}>
{/* Form fields... */}
<button type="submit" disabled={savePost.status === "loading"}>
{savePost.status === "loading" ? "Saving..." : "Save Post"}
</button>
</form>
);
}// Action handler definition
interface ActionHandlerDef {
input?: any;
output?: any;
errorShape: any;
}
// Mutation arguments based on input type
type MutationArgs<TDef extends ActionHandlerDef> = TDef['input'] extends void
? [input?: undefined | void, opts?: TRPCProcedureOptions]
: [input: FormData | TDef['input'], opts?: TRPCProcedureOptions];
// Error handler options
interface ErrorHandlerOptions<TContext> {
ctx: TContext | undefined;
error: TRPCError;
input: unknown;
path: string;
type: ProcedureType;
}
// tRPC response type
interface TRPCResponse<TData, TError> {
result?: {
data: TData;
};
error?: TError;
}Install with Tessl CLI
npx tessl i tessl/npm-trpc--next