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" 😉
apollo79
apollo792y ago
Well, I had the idea of rendering a site from an exported POST API function: https://start.solidjs.com/core-concepts/api-routes
SolidStart Beta Docuentation
SolidStart Beta Documentation
Early release documentation and resources for SolidStart Beta
apollo79
apollo792y ago
But that is not possible and it is porbably good so Since POST isn't meant to return a site I think Did you get it? I am still not exactly sure what it meant...
binajmen
binajmen2y ago
Yeah I don't think so 😉 You could redirect but not sure it solves the problem I'll try to describe this in a discussion this evening, maybe some people are well inspired out there 🙂
const [create, { 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) {
throw new FormError("", {
fieldErrors: parsing.error.formErrors.fieldErrors,
form: formData, // <--- fill in create.input
});
}

try {
await prisma.user.create({ data: parsing.data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
throw new FormError("EMAIL_ALREADY_USED");
}
}
}
});

return (
<div>
<Form>
...
<Input
type="text"
name="givenName"
label="Given name"
value={(create.input?.get("givenName") as string) ?? ""}
/>
const [create, { 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) {
throw new FormError("", {
fieldErrors: parsing.error.formErrors.fieldErrors,
form: formData, // <--- fill in create.input
});
}

try {
await prisma.user.create({ data: parsing.data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
throw new FormError("EMAIL_ALREADY_USED");
}
}
}
});

return (
<div>
<Form>
...
<Input
type="text"
name="givenName"
label="Given name"
value={(create.input?.get("givenName") as string) ?? ""}
/>
So it is very boilerplate but digging a bit further (still), it seems you can provide back the formData in FormError so you can feed the value back in your form (with JS off) but this should be simplified in some ways because that's heavy 😓
apollo79
apollo792y ago
Ok, cool, I'll try this out Yes, definitely Where did you see this? As far as I understand this right, this doesn't do anything different than passing all the stuff (errors) as form URl search param?
binajmen
binajmen2y ago
it is done by solidstart, so I suppose that's one strategy. digging into the source: /node_modules/solid-start/data/FormError.tsx
binajmen
binajmen2y ago
I made a quick summary/copy-paste in a discussion at https://github.com/solidjs/solid-start/discussions/548
GitHub
How to properly handle errors in form? · Discussion #548 · solidjs/...
Hi, I&#39;d like to handle field errors when submitting a form using createServerAction$. I understand I can throw a ResponseError but I can&#39;t find in the doc how to use it. The ultimat...
binajmen
binajmen2y ago
wait & see 🤞
apollo79
apollo792y ago
I saw that there were already concerns about security with this technique: https://github.com/solidjs/solid-start/discussions/238
GitHub
Reducing Cross Site Scripting Chances · Discussion #238 · solidjs/s...
I want to start with I really like how forms are done in solid start. I think the whole project is super cool and I am actively building with it. When playing with it I noticed that when javascript...
binajmen
binajmen2y ago
You certainly don't want to use this strategy with sensitive data. But I believe this is still valid for common forms. I subscribed to the thread.
apollo79
apollo792y ago
Yes, oc. But I would be interested in a way for sensitive data too.
binajmen
binajmen2y ago
True. I wonder how Remix handle this
apollo79
apollo792y ago
I haven't worked with remix so far, so I have no idea. I'm stuck on this problem 😅 , I need this feature to continue working...
binajmen
binajmen2y ago
In a no JS environment, you don't have tons of solution. Either you embed the original form data within the URL (as SolidStart is doing already) Or use localStorage/cookies for sensible data
apollo79
apollo792y ago
True, wondering what I should do though Maybe I shouldn't keep sensitive data, so just do it like solidstart, but removing the sensitive data and let the user type it again I'll probably have to do that. Not only because of sensitive data, but also because I have textareas, that often have a long content and in edge the max URL length is ~ 2000 chars 😆
binajmen
binajmen2y ago
haha effing Edge ;D that would be fair to remove only sensitive data. a bit like a fail login where the email is persisted but not the password seems like a good tradeoff but in a customer complaint form, you want to persist the loooong message of your customer, otherwise he'll be double pissed off 🙂 depends on the case 🤷‍♂️
apollo79
apollo792y ago
Sure, but that is no sensitive data here 😉
binajmen
binajmen2y ago
taking into account the edge limit 😉
apollo79
apollo792y ago
I recently saw a issue you (I think) opened about the session secret being exposed to the client... was that you? And is there a solution for it?
binajmen
binajmen2y ago
It was me you have to remove VITE_ so it is not bundled in the client and move the createCookieSessionStorage() that uses the env variable in /server folder apparently the /server folder is a special one that will never be exposed to the client
apollo79
apollo792y ago
OK, perfect, thanks!