Server Validation
Run locally for transcripts
π¨βπΌ Sadly, browser only validation is not enough. Users can easily change the
DOM using the browser devtools or even POST to our server directly. You always
must validate on the server side. And you'll also want to render the errors from
the server.
The
action
can do some validation, whatever validation it likes (even async
validation), and if there are errors it can send them back in a JSON response.
You get what the action
returns using a hook called useActionData
which is
very similar to useLoaderData
with the exception that it will return
undefined
until the action
has been called. Here's an example of that:import { json, redirect, type ActionFunctionArgs } from '@remix-run/node'
import { useActionData } from '@remix-run/react'
type ActionErrors = {
formErrors: Array<string>
fieldErrors: {
email: Array<string>
}
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const email = formData.get('email')
const errors: ActionErrors = {
formErrors: [],
fieldErrors: {
email: [],
},
}
if (email.length < 3 && !email.includes('@')) {
errors.fieldErrors.email.push('Invalid email')
}
const hasErrors =
errors.formErrors.length ||
Object.values(errors.fieldErrors).some(fieldErrors => fieldErrors.length)
if (hasErrors) {
return json(
{
status: 'error',
errors,
// π¦Ί the as const is here to help with our TypeScript inference
} as const,
{ status: 400 },
)
}
// subscribe the user to the newsletter
return redirect('/success')
}
export default function Subscribe() {
const actionData = useActionData<typeof action>()
const fieldErrors =
actionData?.status === 'error' ? actionData.errors.fieldErrors : null
const formErrors =
actionData?.status === 'error' ? actionData.errors.formErrors : null
return (
<Form method="post">
<label>
Email <input required type="email" />
</label>
{fieldErrors?.email?.length ? (
<ul>
{fieldErrors.email.map(e => (
<li key={e}>{e}</li>
))}
</ul>
) : null}
<button type="submit">Subscribe</button>
</Form>
)
}
The structure of the
action
response is up to you, but I recommend you use the
structure I have laid out above as it handles multiple errors per field and
form-wide errors with the formErrors
.Keep in mind that because
useActionData
returns the action
data, it could be
undefined until the form has been submitted.To help you out a bit, you can even use this component if you'd like:
function ErrorList({ errors }: { errors?: Array<string> | null }) {
return errors?.length ? (
<ul className="flex flex-col gap-1">
{errors.map((error, i) => (
<li key={i} className="text-foreground-destructive text-[10px]">
{error}
</li>
))}
</ul>
) : null
}
This will make it easy to just render a list of errors, and you can give it a
little space too like this:
<div className="min-h-[32px] px-4 pb-3 pt-1">
<ErrorList errors={fieldErrors?.email} />
</div>
Doing things this way will reduce the amount of jumpiness in the UI when the
errors load which is a nicer UX.
You could definitely put that
div
in the ErrorList
component, but I prefer
to limit the amount of spacing styles that are external to the component
itself. Otherwise you'll find yourself needing to customize this in other
places.π¨ Please add some server-side validation to the
action
function and display
any errors returned from that in the form.To test out the server validation, you'll need to temporarily disable the form
validation. Adding a
noValidate
attribute on the form allows you to submit
it even if there are errors (such as an empty input field). Note that the
other attributes will still be enforced by the browser, such as the
maxLength
. We'll improve this next.Your emoji friends will guide you! Thanks!