Closed gbouteiller closed 4 weeks ago
Hi, thank you for this suggestion. I am on board with this. This will most likely be merged in with shapeError
.
Here is a quick update of what it will look like:
This was incredibly tricky to get the types to align, but there is progress! The problem here is that most people will likely put their shape errors function in procedures. In doing that, there is a challenge to actually type the errors correctly with the correct zod schema. Why? Because the zod schemas will most likely get defined in the actions -- which the procedure doesn't have types for.
It was incredibly important for me that even if you define shape error in a procedure, your errors will still be typed based on the final schema defined in your action. Happy to report, it is (somewhat) working! As you can see in the image, the final error type is a combination of the procedure input and action input -- even though the procedure never has access to the action types. Users will need to use a special typedData
object when passing forward these types.
Looking for feedback before it gets merged!
Yes, I understand what you mean, types at that level can be really tricky 😄 but the result looks really promising. Awesome job!
Give it a shot with zsa@0.3.4
! Documentation. Note that shapeError is experimental for now on the basis that I am not 100% sure if API with typedData
is the most intuitive -- looking for feedback.
Hello @IdoPesok , I just wanted to show you the code, it works perfectly! Thank you so much.
actions.ts
"use server"
export const formAction = createServerActionProcedure()
.experimental_shapeError(({err, typedData}) => {
const values = Object.fromEntries(Object.entries(typedData.inputRaw).filter(([name]) => !name.startsWith("$ACTION")))
if (!(err instanceof ZSAError)) return {code: "INTERNAL_SERVER_ERROR" as const, errors: {root: {type: "INTERNAL_SERVER_ERROR"}}, values}
const {code, inputParseErrors, message} = err
const errors = {
root: {type: code, message: message ?? inputParseErrors?.formErrors?.[0]},
...Object.fromEntries(Object.entries(inputParseErrors?.fieldErrors ?? {}).map(([name, errors]) => [name, {message: errors?.[0]}])),
}
return {code, errors, values}
})
.handler(() => {})
.createServerAction()
export const subscribeToNewsletterAction = formAction
.experimental_shapeError(({ctx: {code, errors, values}}) => (code === "CONFLICT" ? {code, errors} : {code, errors, values}))
.input(zNewsletterValues, {type: "state"})
.output(z.boolean())
.handler(async ({input: {email}}) => {
const code = await subscribeToNewsletter(email)
if (code !== "SUCCESS") throw new ZSAError(code)
return true
})
newsletter-form.tsx
"use client"
export default function NewsletterForm() {
const [[success, failure], action, pending] = useActionState(subscribeToNewsletterAction, [null, null])
const form = useForm<NewsletterValues>({
mode: "onTouched",
resolver: zodResolver(zNewsletterValues),
defaultValues: failure?.values ?? defaultNewsletterValues,
errors: failure?.errors,
})
Awesome -- happy to hear!! Code looks great.
In the context of using
input
withtype="state"
and invalid values with progressive enhancement in mind, is there a way of getting these (invalid) values to pass it for instance asdefaultValues
inRHF
(in that way, the form can be filled with the values during server rendering even if javascript is disabled)?actions.ts
newsletter-form.tsx
For now, my workaround is to create an extra function like the one in
custom state
when I can accessformData
directly and pass it through:actions.ts