How to easily work with form validation errors? #460
-
Hello! I'm trying out oRPC coming from next-safe-action and I'm struggling on finding a nice way to work with form validation errors. Below is an example of how you could work with a form and an action with next-safe-action and be able to pick out potential validation errors coming back from invoking the action. What would the equivalent in oRPC look like? // AddPost.tsx
"use client";
import { useEffect, useRef } from "react";
import { useAction } from "next-safe-action/hooks";
import { addPost } from "./AddPost.api";
export function AddPost() {
const {
execute: executeAddPost,
reset,
result,
hasSucceeded,
} = useAction(addPost);
const formRef = useRef<HTMLFormElement | null>(null);
useEffect(() => {
if (hasSucceeded) {
formRef.current?.reset();
reset();
}
}, [hasSucceeded, reset]);
return (
<form
action={executeAddPost}
ref={formRef}
>
<FormField error={result.validationErrors?.title}> // 👈 This is what I'm trying to achieve
<input name="title" placeholder="Title" />
</FormField>
<FormField error={result.validationErrors?.text}> // 👈 This is what I'm trying to achieve
<textarea name="text" placeholder="Text" />
</FormField>
<button type="submit">Add post</button>
{hasSucceeded && (
<p>Post added successfully!</p>
)}
</form>
);
}
interface FormFieldProps {
children: React.ReactNode;
error?: string | string[];
}
function FormField({ children, error }: FormFieldProps) {
return (
<div>
{children}
{error && typeof error === "string" && (
<p>{error}</p>
)}
{error &&
Array.isArray(error) &&
error.map((e) => (
<p key={e}>
{e}
</p>
))}
</div>
);
}
// AddPost.api.ts
"use server"
import { flattenValidationErrors } from "next-safe-action";
import { z } from "zod";
import { zfd } from "zod-form-data";
import { action } from "~/lib/server-action-utils";
const addPostActionSchema = zfd.formData({
title: zfd.text(
z
.string()
.min(5, "Title must be at least 5 characters long.")
.max(50, "Title can be at most 50 characters long.")
),
text: zfd.text(
z
.string()
.min(10, "Text must be at least 10 characters long.")
.max(500, "Text can be at most 500 characters long.")
),
});
export const addPost = action
.schema(addPostActionSchema, {
handleValidationErrorsShape: async (validationErrors) =>
flattenValidationErrors(validationErrors).fieldErrors,
})
.action(
async (
{
parsedInput: { text, title }
}
) => {
// Form action
}
); |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
Unfortunately, oRPC doesn't currently provide a built-in way to handle this. You'll need to manually parse validation errors from errors: https://orpc.unnoq.com/docs/advanced/validation-errors That said, I do have a solution for this at the moment and plan to release it in an upcoming version (1.2 or 1.3). |
Beta Was this translation helpful? Give feedback.
-
Can export function SomeComponent() {
const { execute, error } = useServerAction(someServerAction)
return (
<form action={(form) => { execute(decodeFormData(form)) }}>
<FormField error={getValidationError(error, 'title')}>
<input name="title" placeholder="Title" />
</FormField>
<FormField error={getValidationError(error, 'text')}>
<input name="text" placeholder="Title" />
</FormField>
</form>
)
} decodeFormData utilityimport { StandardBracketNotationSerializer } from '@orpc/openapi-client/standard'
export function decodeFormData(form: FormData): any {
const serializer = new StandardBracketNotationSerializer()
return serializer.deserialize(Array.from(form.entries()))
} getValidationError utilityimport type { SchemaIssue } from '@orpc/contract'
import { ORPCError } from '@orpc/client'
import { StandardBracketNotationSerializer } from '@orpc/openapi-client/standard'
import { isObject } from '@orpc/shared'
export function getValidationError(error: unknown, field: string): string | undefined {
if (!isObject(error) && !(error instanceof ORPCError)) {
return undefined
}
if (!isObject(error.data) || !isStandardSchemaIssues(error.data.issues)) {
return undefined
}
const serializer = new StandardBracketNotationSerializer()
for (const issue of error.data.issues) {
if (issue.path === undefined) {
return field === '' ? issue.message : undefined
}
const issuePath = serializer.stringifyPath(issue.path.map(path => typeof path !== 'object' ? path.toString() : path.key.toString()))
if (issuePath === field) {
return issue.message
}
}
}
function isStandardSchemaIssues(issues: unknown): issues is SchemaIssue[] {
const isPropertyKey = (value: unknown): value is PropertyKey => {
const type = typeof value
return type === 'string' || type === 'number' || type === 'symbol'
}
const isPathSegment = (value: unknown) => {
return isObject(value) && 'key' in value && isPropertyKey(value.key)
}
if (!Array.isArray(issues)) {
return false
}
return issues.every((issue): issue is SchemaIssue => {
if (!isObject(issue)) {
return false
}
if (!('message' in issue && typeof issue.message === 'string')) {
return false
}
if ('path' in issue && issue.path !== undefined) {
const path = issue.path
if (!Array.isArray(path)) {
return false
}
if (!path.every(isPropertyKey) && !path.every(isPathSegment)) {
return false
}
}
return true
})
} |
Beta Was this translation helpful? Give feedback.
Unfortunately, oRPC doesn't currently provide a built-in way to handle this. You'll need to manually parse validation errors from errors: https://orpc.unnoq.com/docs/advanced/validation-errors
That said, I do have a solution for this at the moment and plan to release it in an upcoming version (1.2 or 1.3).