1.0.2 • Published 5 months ago

react-router-action v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

React Router Action

React Router Action defines multiple actions per route and returns typed success and error results. Paired with React Router route module type inference you get fully typed RPC.

Installation

npm i react-router-action

Usage

Create actions and export.

import { createAction, routeAction } from "react-router-action";
import { z } from "zod";

const echo = createAction({
  schema: z.object({
    value: z.string(),
  }),
  handler(ctx) {
    return ctx.data(ctx.input.value);
  },
});

const ping = createAction({
  schema: z.object({}),
  handler(ctx) {
    return ctx.data("pong");
  },
});

export const action = routeAction({
  echo,
  ping,
});

If you have other code to run in the action you may use matchAction instead.

import { matchAction } from "react-router-action";
import type { Route } from "./+types/page";

export async function action(args: Route.ActionArgs) {
  // run other code
  return matchAction(args, {
    echo,
    ping,
  });
}

Submit with form or fetcher.

import { Form } from "react-router";
import type { Route } from "./+types/page";

export default function Page({ actionData }: Route.ComponentProps) {
  return (
    <Form method="POST">
      <input type="hidden" name="_action" value="echo" />
      <div>
        <input type="text" name="value" />
        {actionData?.echo?.fieldErrors?.value && (
          <p>{actionData.echo.fieldErrors.value[0]}</p>
        )}
      </div>

      {actionData?.echo?.error && <p>Error: {actionData.echo.error}</p>}
      {actionData?.echo?.success && <p>Last submit: {actionData.echo.data}</p>}
      <button>Submit</button>
    </Form>
  );
}

Use actionResult or useActionResult hook to reduce optional chaining. Field errors are also flattened by default to return just the first error.

import { Form } from "react-router";
import { useActionResult } from "react-router-action";
import type { Route } from "./+types/page";

export default function Page({ actionData }: Route.ComponentProps) {
  const echoResult = useActionResult(actionData, "echo");

  return (
    <Form method="POST">
      <input type="hidden" name="_action" value="echo" />
      <div>
        <input type="text" name="value" />
        {echoResult.fieldErrors?.value && <p>{echoResult.fieldErrors.value}</p>}
      </div>

      {echoResult.error && <p>Error: {echoResult.error}</p>}
      {echoResult.success && <p>Last submit: {echoResult.data}</p>}
      <button>Submit</button>
    </Form>
  );
}

Returning Responses

Return a response using data or error. You can also pass status code or response headers. By default a 200 status code is sent for data responses while error responses require an explcit status code. Error responses can also contain field errors.

const ping = createAction({
  schema: z.object({}),
  handler(ctx) {
    // data response with status
    const res1 = ctx.data("pong", 200);

    // data response with status and headers
    const res2 = ctx.data("pong", {
      status: 200,
      headers: {
        // ...custom headers
      },
    });

    // error response with status
    const res2 = ctx.error("failed", 400);

    // error response with status and field errors
    const res2 = ctx.error("failed", 400, {
      // ...field errors
    });
  },
});

Throw responses like redirects so they don't affect the returned type.

const ping = createAction({
  schema: z.object({}),
  handler(ctx) {
    throw redirect("/login");
  },
});

Field Errors

Field errors are returned with a 400 status code when input validation fails. You can also return explicit field errors in the action handler.

const echo = createAction({
  schema: z.object({
    value: z.string(),
  }),
  handler(ctx, args: Route.ActionArgs) {
    if (ctx.input.value !== args.params.id) {
      return ctx.error("Invalid data", 400, {
        value: ["value must match id param"],
      });
    }
    return ctx.data(ctx.input.value);
  },
});

When using actionResult or useActionResult the fieldErrors are flattened to return only the first error by default. See the example below to return an array of errors.

import { Form } from "react-router";
import { useActionResult } from "react-router-action";
import type { Route } from "./+types/page";

export default function Page({ actionData }: Route.ComponentProps) {
  const echoResult = useActionResult(actionData, "echo", { errors: "all" });

  return (
    <Form method="POST">
      <input type="hidden" name="_action" value="echo" />
      <div>
        <input type="text" name="value" />
        {echoResult.fieldErrors?.value && (
          <ul>
            {echoResult.fieldErrors.value.map((error) => (
              <li>{error}</li>
            ))}
          </ul>
        )}
      </div>

      {echoResult.error && <p>Error: {echoResult.error}</p>}
      {echoResult.success && <p>Last submit: {echoResult.data}</p>}
      <button>Submit</button>
    </Form>
  );
}

Action Args

Action args is the second argument to the action handler. Explicitly type action args to use auto-generated types.

import { createAction } from "react-router-action";
import { z } from "zod";
import type { Route } from "./+types/page";

const echo = createAction({
  schema: z.object({
    value: z.string(),
  }),
  handler(ctx, args: Route.ActionArgs) {
    // action args is typed correctly
  },
});
1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago