How to return field error(s) from server action?

Hi, I'd like to handle field errors when submitting a form using createServerAction$. I understand I can throw a ResponseError but I can't find in the doc how to use it.
35 Replies
binajmen
binajmen2y ago
The ultimate goal is to highlight the fields with errors and provide a useful description (validation done with zod) Coming from Remix, I'm looking for something similar to useActionData 🙈
binajmen
binajmen2y ago
In this example https://start.solidjs.com/core-concepts/actions#using-forms-to-submit-data, we throw a new Error("invalid username").
SolidStart Beta Docuentation
SolidStart Beta Documentation
Early release documentation and resources for SolidStart Beta
binajmen
binajmen2y ago
How can we industrialise this for a more complex form when using zod validation? Can I pass an object/json instead? I was thinking about this for the meantime:
const [{ result }, { Form }] = createServerAction$(
async (formData: FormData) => {
const parsing = z
.object({
email: z.string().email(),
givenName: z.string(),
familyName: z.string(),
age: z.preprocess((v) => Number(v), z.number()),
gender: z.enum(["male", "female", "other"]),
})
.safeParse(Object.fromEntries(formData.entries()));

if (!parsing.success) {
return { success: false, errors: parsing.error.flatten() };
}

await prisma.user.create({ data: parsing.data });
return { success: true };
}
);

const [, deleteUser] = createServerAction$(async (id: string) => {
await prisma.user.delete({ where: { id } });
});

createEffect(() => console.log(result?.success));
const [{ result }, { Form }] = createServerAction$(
async (formData: FormData) => {
const parsing = z
.object({
email: z.string().email(),
givenName: z.string(),
familyName: z.string(),
age: z.preprocess((v) => Number(v), z.number()),
gender: z.enum(["male", "female", "other"]),
})
.safeParse(Object.fromEntries(formData.entries()));

if (!parsing.success) {
return { success: false, errors: parsing.error.flatten() };
}

await prisma.user.create({ data: parsing.data });
return { success: true };
}
);

const [, deleteUser] = createServerAction$(async (id: string) => {
await prisma.user.delete({ where: { id } });
});

createEffect(() => console.log(result?.success));
But the createEffect only prints undefined. Pretty sure I'm doing something wrong.. 😅 Digging further, I feel I should do that:
if (!parsing.success) {
throw new FormError("Form error", {
fieldErrors: parsing.error.formErrors.fieldErrors,
});
}
if (!parsing.success) {
throw new FormError("Form error", {
fieldErrors: parsing.error.formErrors.fieldErrors,
});
}
And do this:
<Show when={add.error}>
<p>There was an error</p>
</Show>
<Show when={add.error}>
<p>There was an error</p>
</Show>
What I'm struggling to find is how to retrieve the fieldErrors so I can provide fine-grained error explanation (field by field) Actually, it is available in add.error.fieldErrors but it is not typed. Is there a way to type it? Going forward, how can I simplify this:
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{add.error?.fieldErrors["email"] &&
add.error.fieldErrors["email"].join(", ")}
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{add.error?.fieldErrors["email"] &&
add.error.fieldErrors["email"].join(", ")}
I was trying this:
const errors = add.error?.fieldErrors || {};

return (
<div>
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{errors["email"] && errors["email"].join(", ")}
const errors = add.error?.fieldErrors || {};

return (
<div>
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{errors["email"] && errors["email"].join(", ")}
But because Solid is awesome, errors is defined only one time, and add.error is not a signal... So I can't make errors reactive and it will always be equal to {}
apollo79
apollo792y ago
I currently have the same problem with actions and server side validating, because if JS is turned off, you'll see, that the form gets submitted to a different page and then it gets complicated to show the original form with errors, but for the const errors it should work, if you just make a function:
const errors = () => add.error?.fieldErrors || {};

return (
<div>
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{errors().email && errors().email?.join(", ")}
const errors = () => add.error?.fieldErrors || {};

return (
<div>
<Form>
<h1>Add new user</h1>
<input type="email" name="email" />
{errors().email && errors().email?.join(", ")}
binajmen
binajmen2y ago
Han, I didn't know this "into a function" trick. Is it like an implicit signal? I didn't test with JS off, thanks for pointing that out. I'm wondering how it should be resolved. Perhaps using URLSearchParams. It feels like a hack, although Tanner Linsley was very clear in a talk that URL is the perfect local state manager!
apollo79
apollo792y ago
Yes, from the docs:
// read signal's current value, and
// depend on signal if in a tracking scope
// (but nonreactive outside of a tracking scope):
const currentCount = count();

// or wrap any computation with a function,
// and this function can be used in a tracking scope:
const doubledCount = () => 2 * count();

// or build a tracking scope and depend on signal:
const countDisplay = <div>{count()}</div>;

// write signal by providing a value:
setReady(true);

// write signal by providing a function setter:
const newCount = setCount((prev) => prev + 1);
// read signal's current value, and
// depend on signal if in a tracking scope
// (but nonreactive outside of a tracking scope):
const currentCount = count();

// or wrap any computation with a function,
// and this function can be used in a tracking scope:
const doubledCount = () => 2 * count();

// or build a tracking scope and depend on signal:
const countDisplay = <div>{count()}</div>;

// write signal by providing a value:
setReady(true);

// write signal by providing a function setter:
const newCount = setCount((prev) => prev + 1);
binajmen
binajmen2y ago
But it's because doubleCount refer to count() that is a signal, no?
apollo79
apollo792y ago
Yes, maybe, or a cookie. I don't know, cause my form uses POST, I don't think URLSearchParams is the right way for that...
binajmen
binajmen2y ago
I was more thinking about returning a redirect holding params.. But it really feels hacky 🙂
apollo79
apollo792y ago
Yes, this feels really hacky. And the URL is kinda messed up Iin my form, there are description fields holding loooooong strings...
binajmen
binajmen2y ago
The talk of Tanner was very inspiring, explaining among other things that we should not care about the URL content that much.. Most of the people just don't watch what's inside. That being said, I'd love to see what is the best practice regarding form handling I'll create a discussion on github to track this question
apollo79
apollo792y ago
Yes... only the crawlers 😉 It would be cool to have the option to render from the POST function... But it's only meant for APIs. I recently asked this in the solid-start channel: https://discordapp.com/channels/722131463138705510/910635844119982080/1053053903907860500
binajmen
binajmen2y ago
Why are you using the API form?
const [create, { Form }] = createServerAction$(async (formData: FormData) => {
// do something with formData
});
const [create, { Form }] = createServerAction$(async (formData: FormData) => {
// do something with formData
});
Or perhaps I misunderstand your use case
apollo79
apollo792y ago
Sry, I don't get this question 😅
binajmen
binajmen2y ago
I need a decipher to understand the answer of ryan 😅 Disregard it, I'm not sure what I meant. I suppose you use createServerAction$ as well, and behind the scene it's a POST call which explains why you mentioned "POST function" 😉
Want results from more Discord servers?
Add your server