CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-trpc--next

Next.js integration for tRPC that enables end-to-end type-safe APIs with enhanced server-side rendering and static site generation capabilities.

Pending
Overview
Eval results
Files

server-actions.mddocs/

Server Actions

Experimental server actions integration for form handling and mutations in Next.js App Router.

Capabilities

Action Hook Creation

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>
  );
}

Server Action Handler Creation

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);
}

Server Action Link

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,
});

Action Result States

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';
}

Advanced Usage

Form Data Handling

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>
  );
}

Error Handling

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...
}

Progressive Enhancement

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>
  );
}

Types

// 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

docs

app-router.md

cache-links.md

index.md

pages-router.md

server-actions.md

ssr.md

tile.json