Also known as #remix.

  • throw from actions will naturally be caught by the nearest ErrorBoundary.
  • Route.ComponentProps['actionData'] is only useful client-side. Don't spend x-minutes wondering why your console.log insists on being undefined.
  • Instead of throwing from an action, you can try/catch and return data() with a status: 400.
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData()
  const emailAddress = formData.get("email_address")

  if (typeof emailAddress !== "string") {
    throw new Error("Expected a string value, but received a File.")
  }

  try {
    const validEmailAddress = z.string().email().parse(emailAddress)

    await someSideEffect(validEmailAddress) // like authentication

    return data({ ok: true as const, emailAddress }, { status: 200 })
  } catch (error) {
    if (error instanceof ZodError) {
      return data(
        { ok: false as const, emailAddress, error: "validation" as const },
        { status: 400 },
      )
    }

    return data(
      { ok: false as const, emailAddress, error: "unknown" as const },
      { status: 400 },
    )
  }
}

export default function Route({
  loaderData,
  actionData,
}: Route.ComponentProps) {

  // @NOTE: actionData only exists client-side
  return (
    <>
      {actionData?.ok ? (
        <p>hooray!</p>
      ) : (
      <Form method="post">
        <input
          name="email_address"
          placeholder="Email address"
          required type="email"
        />
        <button>submit</button>
      </Form>
      )}
    </>
  )
}

Notes #

  • I think it is bizarre that react-router documentation uses let everywhere.
  • It's also weird to throw their redirect from actions. That ain't an error.