useActionData() returns whatever the current route's action returned, from its most recent call. Before any submission it is undefined, which makes it the natural home for validation errors rendered next to the fields that caused them.
function Signup() {
const r = useActionData(); // undefined until a submit
const field = (name, label) => <>
<label>{label} <input name={name} defaultValue={r?.values?.[name]} /></label>
{r?.errors?.[name] && <p className="err">{r.errors[name]}</p>}</>;
return <Form method="post">{field('username', 'Username')}
{field('email', 'Email')}<button type="submit">Sign up</button></Form>;
}The route's action is the one from Route Actions: validate, and return { errors, values } instead of redirecting when a field is wrong. Submitting jo and jo.example.com leaves the user on the same route with both messages under their fields and both typed values back in the inputs, because r.values came along for the ride.
useLoaderData() always has a value, because its loader ran before the component existed; useActionData() starts empty and fills in after a round trip. It is scoped to the route that owns the action, which is why a successful action normally ends in redirect().